diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 91106d3..1bc9a31 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,17 +1,30 @@ name: Java CI -on: [push] +on: + push: + branches: [ master, feature/** ] + pull_request: + branches: [ master ] jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 - with: - java-version: 1.8 - - name: Build with Maven - run: mvn -B package --file pom.xml + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + + - name: Build with Maven + run: mvn -B clean install --file pom.xml + + - name: Upload coverage to Coveralls + if: github.event_name != 'pull_request' + run: mvn coveralls:report -DrepoToken=${{ secrets.COVERALLS_TOKEN }} + continue-on-error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..54130c1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,49 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + + - name: Extract version from tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Build and deploy to Maven Central + run: | + mvn -B clean deploy -P release \ + -Drevision=${{ steps.get_version.outputs.VERSION }} \ + -DskipTests + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true + files: | + **/target/*.jar + !**/target/*-sources.jar + !**/target/*-javadoc.jar diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index c32394f..0000000 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.5"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 0d5e649..0000000 Binary files a/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 15fbe3d..8dea6c2 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,2 +1,3 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 0000000..15465fd --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,5 @@ +# SDKMAN configuration for friendly-id project +# Auto-switch to this Java version when entering the directory +# Usage: Enable auto-env in SDKMAN with: sdk config +# Set sdkman_auto_env=true +java=21.0.8-tem diff --git a/.serena/cache/java/document_symbols_cache_v23-06-25.pkl b/.serena/cache/java/document_symbols_cache_v23-06-25.pkl new file mode 100644 index 0000000..c21ae48 Binary files /dev/null and b/.serena/cache/java/document_symbols_cache_v23-06-25.pkl differ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6e73095 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- FriendlyId value object type (`com.devskiller.friendly_id.type.FriendlyId`) as an alternative to raw UUID +- JPA integration module (`friendly-id-jpa`) with automatic AttributeConverter +- OpenFeign integration module (`friendly-id-openfeign`) for FriendlyId support in Feign clients +- jOOQ integration module (`friendly-id-jooq`) for FriendlyId support in jOOQ +- Spring Boot JPA demo application showcasing FriendlyId with JPA, REST API, and OpenFeign + +### Changed +- Upgraded from Java 8 to Java 21 +- Upgraded from Spring Boot 2.2.2 to 3.4.1 +- Upgraded from JUnit 4 to JUnit 5 +- Migrated from Vavr property testing to JUnit 5 `@RepeatedTest` +- Updated Spring Boot auto-configuration to use `AutoConfiguration.imports` instead of `spring.factories` + +### Fixed +- Fixed Jackson module serialization by adding `super.setupModule(context)` call in `FriendlyIdModule` +- Fixed Spring Cloud version compatibility (2024.0.0 for Spring Boot 3.4.1) +- Added `friendly-id-jackson-datatype` as dependency to `friendly-id-spring-boot-starter` for complete auto-configuration +- Added `friendly-id-jackson-datatype` as dependency to `friendly-id-openfeign` for JSON serialization support + +### Dependencies +- `friendly-id-spring-boot-starter` now includes `friendly-id-jackson-datatype` transitively +- `friendly-id-openfeign` now includes `friendly-id-jackson-datatype` transitively + +### Infrastructure +- Migrated from legacy Sonatype OSSRH to Central Portal for Maven Central publishing +- Updated `central-publishing-maven-plugin` to 0.6.0 for automated publishing + +## [1.1.0] - Previous version +- Legacy implementation with UUID-only support diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000..d4c86a8 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,160 @@ +# Publishing to Maven Central + +This document describes how to publish `friendly-id` artifacts to Maven Central using the new Sonatype Central Portal. + +## Prerequisites + +1. **Account on Central Portal**: https://central.sonatype.com +2. **Namespace verified**: `com.devskiller.friendly-id` +3. **GPG Key**: For signing artifacts +4. **Maven credentials**: Token configured in `~/.m2/settings.xml` + +## Configuration + +### 1. Maven Settings (`~/.m2/settings.xml`) + +Add your Central Portal credentials: + +```xml + + + central + YOUR_USERNAME + YOUR_TOKEN + + +``` + +### 2. GPG Key Setup + +Generate GPG key if you don't have one: + +```bash +# Generate key +gpg --gen-key + +# List keys +gpg --list-keys + +# Export public key to keyserver +gpg --keyserver keyserver.ubuntu.com --send-keys YOUR_KEY_ID +``` + +## Publishing Process + +### Option 1: Snapshot Release (for testing) + +```bash +# Build and deploy snapshot +mvn clean deploy +``` + +Snapshots will be available at: +``` +https://central.sonatype.com/artifact/com.devskiller.friendly-id/friendly-id/1.1.1-SNAPSHOT +``` + +### Option 2: Release Version + +1. **Update version** (remove `-SNAPSHOT` suffix): +```bash +# Update version in pom.xml +mvn versions:set -DnewVersion=1.1.1 +mvn versions:commit +``` + +2. **Build and deploy with release profile**: +```bash +# This will: +# - Create source JARs +# - Create javadoc JARs +# - Sign all artifacts with GPG +# - Deploy to Central Portal +mvn clean deploy -Prelease +``` + +3. **Verify deployment**: +- Go to https://central.sonatype.com/publishing/deployments +- Check deployment status +- Artifacts will be automatically published to Maven Central (autoPublish=true) + +4. **Tag the release**: +```bash +git tag -a 1.1.1 -m "Release version 1.1.1" +git push origin 1.1.1 +``` + +5. **Prepare next development version**: +```bash +mvn versions:set -DnewVersion=1.1.2-SNAPSHOT +mvn versions:commit +git add pom.xml */pom.xml +git commit -m "chore: prepare next development version 1.1.2-SNAPSHOT" +git push +``` + +## Troubleshooting + +### GPG Signing Issues + +If you get "gpg: signing failed: Inappropriate ioctl for device": +```bash +export GPG_TTY=$(tty) +``` + +Or add to `~/.bashrc`: +```bash +export GPG_TTY=$(tty) +``` + +### Wrong credentials + +Make sure `central` in settings.xml matches `central` in pom.xml. + +### Deployment verification + +Check deployment status: +```bash +# List recent deployments +curl -u "YOUR_USERNAME:YOUR_TOKEN" \ + https://central.sonatype.com/api/v1/publisher/deployments +``` + +## Maven Central Sync + +After successful deployment: +- Artifacts are **immediately available** on Central Portal +- Sync to Maven Central (repo1.maven.org) takes **10-30 minutes** +- Search index update (search.maven.org) takes **up to 2 hours** + +## Verification + +After publication, verify artifacts are available: + +```bash +# Check on Central Portal +curl https://central.sonatype.com/artifact/com.devskiller.friendly-id/friendly-id/1.1.1 + +# Check on Maven Central (after sync) +curl https://repo1.maven.org/maven2/com/devskiller/friendly-id/friendly-id/1.1.1/ +``` + +## CI/CD Integration (Future) + +For automated releases via GitHub Actions, you'll need to: + +1. Add GitHub Secrets: + - `MAVEN_CENTRAL_USERNAME` + - `MAVEN_CENTRAL_TOKEN` + - `GPG_PRIVATE_KEY` + - `GPG_PASSPHRASE` + +2. Create `.github/workflows/release.yml` workflow + +3. Use `central-publishing-maven-plugin` in the workflow + +## References + +- [Central Portal Documentation](https://central.sonatype.org/publish/publish-portal-maven/) +- [Requirements](https://central.sonatype.org/publish/requirements/) +- [Central Publishing Maven Plugin](https://central.sonatype.org/publish/publish-portal-maven/) diff --git a/README.md b/README.md index e3489da..cd1d15b 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Dependencies com.devskiller.friendly-id friendly-id - 1.1.0 + 2.0.0-alpha6 ``` @@ -147,9 +147,11 @@ Notes ## Integrations - - [Spring Boot integration](#Spring-Boot-integration) -- [Jackson integration ](#Jackson-integration) +- [Jackson integration](#Jackson-integration) +- [jOOQ integration](#jOOQ-integration) +- [JPA integration](#JPA-integration) +- [OpenFeign integration](#OpenFeign-integration) ### Spring Boot integration @@ -159,7 +161,7 @@ The FriendlyID library includes a Spring configuration to make it easy to add sh com.devskiller.friendly-id friendly-id-spring-boot-starter - 1.1.0 + 2.0.0-alpha6 ``` @@ -203,7 +205,7 @@ First, add the following Jackson module dependency: com.devskiller.friendly-id friendly-id-jackson-datatype - 1.1.0 + 2.0.0-alpha6 ``` Then register the `FriendlyIdModule` module as follows: @@ -213,6 +215,89 @@ ObjectMapper mapper = new ObjectMapper() .registerModule(new FriendlyIdModule()); ``` +### jOOQ integration + +The FriendlyID library provides a jOOQ converter for seamless integration with jOOQ's code generation and type-safe queries. + +First, add the dependency: +```xml + + com.devskiller.friendly-id + friendly-id-jooq + 2.0.0-alpha6 + +``` + +Configure the converter in your jOOQ code generation configuration: +```xml + + + com.devskiller.friendly_id.type.FriendlyId + com.devskiller.friendly_id.jooq.FriendlyIdConverter + .*\.id + UUID + + +``` + +This automatically converts UUID database columns to `FriendlyId` value objects in your generated jOOQ records. + +### JPA integration + +The FriendlyID library includes a JPA `AttributeConverter` for transparent conversion between UUID database columns and `FriendlyId` value objects. + +First, add the dependency: +```xml + + com.devskiller.friendly-id + friendly-id-jpa + 2.0.0-alpha6 + +``` + +The converter is automatically applied to all `FriendlyId` attributes in your entities: +```java +@Entity +public class User { + @Id + private FriendlyId id; + + private String name; + + // getters/setters +} +``` + +The `FriendlyId` value object stores UUID internally (16 bytes) and computes the FriendlyId string only when needed, making it more memory-efficient than storing strings. + +### OpenFeign integration + +The FriendlyID library provides automatic encoding/decoding for Spring Cloud OpenFeign clients. + +First, add the dependency: +```xml + + com.devskiller.friendly-id + friendly-id-openfeign + 2.0.0-alpha6 + +``` + +The integration is automatically configured when Spring Cloud OpenFeign is on the classpath: +```java +@FeignClient(name = "user-service") +public interface UserClient { + + @GetMapping("/users/{id}") + UserDto getUser(@PathVariable UUID id); // Sends FriendlyId string + + @GetMapping("/users/{id}/profile") + ProfileDto getProfile(@PathVariable FriendlyId id); // Also works with FriendlyId value object +} +``` + +UUID and `FriendlyId` parameters are automatically converted to FriendlyId strings in requests, and FriendlyId strings in responses are converted back to UUID or `FriendlyId` objects. + Contributing ---------- diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..8a4d354 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,151 @@ +# Releasing Guide + +This document describes how to release a new version of FriendlyID to Maven Central. + +## Prerequisites + +Before releasing, ensure you have: + +1. **GitHub Repository Access**: Write access to push tags +2. **GitHub Secrets Configured**: The following secrets must be set in repository settings: + - `OSSRH_USERNAME` - Sonatype OSSRH username + - `OSSRH_TOKEN` - Sonatype OSSRH token/password + - `GPG_PRIVATE_KEY` - GPG private key for artifact signing + - `GPG_PASSPHRASE` - Passphrase for the GPG key + +## Release Process + +### 1. Prepare for Release + +Ensure your local repository is up to date and all tests pass: + +```bash +git checkout master +git pull origin master +mvn clean install +``` + +### 2. Create Release Tag + +Create an annotated tag with the version number (must start with `v`): + +```bash +# For release version (e.g., 1.2.0) +git tag -a v1.2.0 -m "Release 1.2.0" + +# For release candidate (e.g., 1.2.0-RC1) +git tag -a v1.2.0-RC1 -m "Release 1.2.0-RC1" +``` + +### 3. Push Tag to GitHub + +Push the tag to trigger the automated release: + +```bash +git push origin v1.2.0 +``` + +### 4. Monitor Release Process + +1. Go to **Actions** tab in GitHub repository +2. Watch the **Release** workflow execution +3. The workflow will: + - Build all modules with the specified version + - Run all tests (can be skipped with `-DskipTests`) + - Sign artifacts with GPG + - Deploy to Maven Central (OSSRH) + - Create GitHub Release with artifacts + +### 5. Verify Release + +After successful deployment: + +1. Check [Maven Central Repository](https://repo1.maven.org/maven2/com/devskiller/friendly-id/) +2. Verify the GitHub Release was created with artifacts +3. Test the release in a separate project: + +```xml + + com.devskiller.friendly-id + friendly-id + 1.2.0 + +``` + +## Version Numbering + +Follow [Semantic Versioning](https://semver.org/): + +- **MAJOR** version (X.0.0): Incompatible API changes +- **MINOR** version (0.X.0): New functionality, backwards-compatible +- **PATCH** version (0.0.X): Backwards-compatible bug fixes + +Examples: +- `v1.0.0` - Initial release +- `v1.1.0` - New feature (e.g., OpenFeign integration) +- `v1.1.1` - Bug fix +- `v2.0.0` - Breaking change (e.g., Java version upgrade) + +## Snapshot Releases + +Snapshot versions are built automatically on every push to `master` branch but are NOT deployed to Maven Central. They use the version defined in the `` property in the parent POM. + +## Manual Release (Emergency) + +If automated release fails, you can release manually: + +```bash +# Build and deploy to Maven Central +mvn clean deploy -P release -Drevision=1.2.0 + +# Create GitHub release manually through GitHub UI +``` + +## Rollback + +If a release needs to be rolled back: + +1. **Do NOT delete tags from Maven Central** - versions are immutable +2. Delete the GitHub tag and release: + ```bash + git tag -d v1.2.0 + git push origin :refs/tags/v1.2.0 + ``` +3. Release a new patch version with the fix + +## Troubleshooting + +### GPG Signing Fails + +- Verify `GPG_PRIVATE_KEY` secret is correctly formatted (including `-----BEGIN PGP PRIVATE KEY BLOCK-----`) +- Check `GPG_PASSPHRASE` is correct +- Ensure GPG key hasn't expired + +### Maven Central Deployment Fails + +- Verify `OSSRH_USERNAME` and `OSSRH_TOKEN` are correct +- Check [OSSRH Status](https://status.maven.org/) +- Review deployment logs in GitHub Actions + +### Build Fails + +- Check all tests pass locally: `mvn clean install` +- Review GitHub Actions logs for specific error +- Ensure all dependencies are available in Maven Central + +## Post-Release Tasks + +After successful release: + +1. Update `` in parent `pom.xml` to next SNAPSHOT version +2. Update version in `README.md` examples (if needed) +3. Announce release (GitHub Discussions, Twitter, etc.) +4. Close related GitHub issues/PRs + +## CI-Friendly Versioning + +This project uses [Maven CI-friendly versioning](https://maven.apache.org/guides/mini/guide-maven-ci-friendly.html). The version is controlled by the `${revision}` property: + +- **Default**: Defined in parent `pom.xml` (`1.1.1-SNAPSHOT`) +- **Override**: `mvn -Drevision=X.Y.Z` +- **Release**: GitHub Actions sets version from git tag diff --git a/friendly-id-jackson-datatype/pom.xml b/friendly-id-jackson-datatype/pom.xml index fe2ed3d..c6eb295 100644 --- a/friendly-id-jackson-datatype/pom.xml +++ b/friendly-id-jackson-datatype/pom.xml @@ -5,38 +5,39 @@ friendly-id-project com.devskiller.friendly-id - 1.1.1-SNAPSHOT + ${revision} .. friendly-id-jackson-datatype + FriendlyId Jackson Datatype + Jackson module for JSON serialization/deserialization of UUIDs as FriendlyIds + com.devskiller.friendly-id friendly-id ${project.version} + com.fasterxml.jackson.core jackson-annotations + - com.fasterxml.jackson.core + tools.jackson.core jackson-core - com.fasterxml.jackson.core + tools.jackson.core jackson-databind - - com.fasterxml.jackson.module - jackson-module-parameter-names - - junit - junit + org.junit.jupiter + junit-jupiter test @@ -49,8 +50,8 @@ + org.apache.maven.plugins maven-compiler-plugin - 3.8.1 true diff --git a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java index 3e0d89a..5a56f9c 100644 --- a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java +++ b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java @@ -1,33 +1,75 @@ package com.devskiller.friendly_id.jackson; -import java.io.IOException; import java.util.UUID; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.deser.std.UUIDDeserializer; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.BeanProperty; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.deser.std.StdDeserializer; import com.devskiller.friendly_id.FriendlyId; +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; -public class FriendlyIdDeserializer extends UUIDDeserializer { +public class FriendlyIdDeserializer extends StdDeserializer { - @Override - public UUID deserialize(JsonParser parser, DeserializationContext deserializationContext) throws IOException { + private final boolean useFriendlyFormat; + + public FriendlyIdDeserializer() { + this(true); + } - JsonToken token = parser.getCurrentToken(); + private FriendlyIdDeserializer(boolean useFriendlyFormat) { + super(UUID.class); + this.useFriendlyFormat = useFriendlyFormat; + } + + @Override + public UUID deserialize(JsonParser parser, DeserializationContext ctxt) { + var token = parser.currentToken(); if (token == JsonToken.VALUE_STRING) { - String string = parser.getValueAsString().trim(); - if (looksLikeUuid(string)) { - return super.deserialize(parser, deserializationContext); + var value = parser.getString().trim(); + if (useFriendlyFormat) { + return parseAsUuidOrFriendlyId(value); } else { - return FriendlyId.toUuid(string); + return UUID.fromString(value); } } - throw new IllegalStateException("This is not friendly id"); + throw ctxt.weirdStringException(parser.getString(), UUID.class, "Expected UUID string value"); + } + + /** + * Attempts to parse the value as a standard UUID first, then falls back to FriendlyId format. + * This approach is more robust than heuristic-based detection. + */ + private UUID parseAsUuidOrFriendlyId(String value) { + if (isStandardUuidFormat(value)) { + return UUID.fromString(value); + } + return FriendlyId.toUuid(value); + } + + /** + * Checks if the string matches standard UUID format (36 chars with hyphens at positions 8, 13, 18, 23). + */ + private boolean isStandardUuidFormat(String value) { + return value.length() == 36 + && value.charAt(8) == '-' + && value.charAt(13) == '-' + && value.charAt(18) == '-' + && value.charAt(23) == '-'; } - private boolean looksLikeUuid(String value) { - return value.contains("-"); + @Override + public ValueDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { + if (property != null) { + var annotation = property.getAnnotation(IdFormat.class); + if (annotation != null && annotation.value() == FriendlyIdFormat.RAW) { + return new FriendlyIdDeserializer(false); + } + } + return this; } } diff --git a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java index c94369f..1401403 100644 --- a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java +++ b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java @@ -2,20 +2,28 @@ import java.util.UUID; -import com.fasterxml.jackson.databind.module.SimpleModule; +import tools.jackson.databind.module.SimpleModule; -public class FriendlyIdModule extends SimpleModule { +import com.devskiller.friendly_id.type.FriendlyId; - private FriendlyIdAnnotationIntrospector introspector; +/** + * Jackson 3 module for FriendlyId serialization/deserialization. + *

+ * This module registers custom serializers and deserializers for UUID and FriendlyId types, + * enabling automatic conversion between UUID values and their FriendlyId string representation. + *

+ */ +public class FriendlyIdModule extends SimpleModule { public FriendlyIdModule() { - introspector = new FriendlyIdAnnotationIntrospector(); - addDeserializer(UUID.class, new FriendlyIdDeserializer()); + super("FriendlyIdModule"); + + // UUID serializers/deserializers addSerializer(UUID.class, new FriendlyIdSerializer()); - } + addDeserializer(UUID.class, new FriendlyIdDeserializer()); - @Override - public void setupModule(SetupContext context) { - context.insertAnnotationIntrospector(introspector); + // FriendlyId value object serializers/deserializers + addSerializer(FriendlyId.class, new FriendlyIdValueSerializer()); + addDeserializer(FriendlyId.class, new FriendlyIdValueDeserializer()); } } diff --git a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdSerializer.java b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdSerializer.java index f80b803..85284e5 100644 --- a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdSerializer.java +++ b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdSerializer.java @@ -1,22 +1,47 @@ package com.devskiller.friendly_id.jackson; -import java.io.IOException; import java.util.UUID; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.BeanProperty; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.ser.std.StdSerializer; import com.devskiller.friendly_id.FriendlyId; +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; public class FriendlyIdSerializer extends StdSerializer { + private final boolean useFriendlyFormat; + public FriendlyIdSerializer() { + this(true); + } + + private FriendlyIdSerializer(boolean useFriendlyFormat) { super(UUID.class); + this.useFriendlyFormat = useFriendlyFormat; + } + + @Override + public void serialize(UUID uuid, JsonGenerator jsonGenerator, SerializationContext ctxt) { + if (useFriendlyFormat) { + jsonGenerator.writeString(FriendlyId.toFriendlyId(uuid)); + } else { + jsonGenerator.writeString(uuid.toString()); + } } @Override - public void serialize(UUID uuid, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(FriendlyId.toFriendlyId(uuid)); + public ValueSerializer createContextual(SerializationContext ctxt, BeanProperty property) { + if (property != null) { + IdFormat annotation = property.getAnnotation(IdFormat.class); + if (annotation != null && annotation.value() == FriendlyIdFormat.RAW) { + return new FriendlyIdSerializer(false); + } + } + return this; } } diff --git a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java new file mode 100644 index 0000000..1a515c8 --- /dev/null +++ b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java @@ -0,0 +1,24 @@ +package com.devskiller.friendly_id.jackson; + +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.deser.std.StdDeserializer; + +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * JSON deserializer for {@link FriendlyId} value object. + * Deserializes JSON strings to FriendlyId instances. + */ +public class FriendlyIdValueDeserializer extends StdDeserializer { + + public FriendlyIdValueDeserializer() { + super(FriendlyId.class); + } + + @Override + public FriendlyId deserialize(JsonParser p, DeserializationContext ctxt) { + String friendlyIdString = p.getString(); + return FriendlyId.fromString(friendlyIdString); + } +} diff --git a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java new file mode 100644 index 0000000..b2d87fc --- /dev/null +++ b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java @@ -0,0 +1,23 @@ +package com.devskiller.friendly_id.jackson; + +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ser.std.StdSerializer; + +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * JSON serializer for {@link FriendlyId} value object. + * Serializes FriendlyId instances as their string representation. + */ +public class FriendlyIdValueSerializer extends StdSerializer { + + public FriendlyIdValueSerializer() { + super(FriendlyId.class); + } + + @Override + public void serialize(FriendlyId value, JsonGenerator gen, SerializationContext ctxt) { + gen.writeString(value.toString()); + } +} diff --git a/friendly-id-jackson-datatype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/friendly-id-jackson-datatype/src/main/resources/META-INF/services/tools.jackson.databind.JacksonModule similarity index 100% rename from friendly-id-jackson-datatype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module rename to friendly-id-jackson-datatype/src/main/resources/META-INF/services/tools.jackson.databind.JacksonModule diff --git a/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/Bar.java b/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/Bar.java index ba9fb63..46eee9f 100644 --- a/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/Bar.java +++ b/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/Bar.java @@ -2,28 +2,10 @@ import java.util.UUID; -import com.devskiller.friendly_id.jackson.FriendlyIdFormat; -import com.devskiller.friendly_id.jackson.IdFormat; +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; -public class Bar { - - @IdFormat(FriendlyIdFormat.RAW) - private final UUID rawUuid; - - private final UUID friendlyId; - - public Bar(UUID rawUuid, UUID friendlyId) { - this.rawUuid = rawUuid; - this.friendlyId = friendlyId; - } - - public UUID getRawUuid() { - return rawUuid; - } - - public UUID getFriendlyId() { - return friendlyId; - } - - -} +public record Bar( + @IdFormat(FriendlyIdFormat.RAW) UUID rawUuid, + UUID friendlyId +) {} diff --git a/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/FieldWithoutFriendlyIdTest.java b/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/FieldWithoutFriendlyIdTest.java index 2482dc5..734510f 100644 --- a/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/FieldWithoutFriendlyIdTest.java +++ b/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/FieldWithoutFriendlyIdTest.java @@ -2,69 +2,63 @@ import java.util.UUID; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; -import org.junit.Test; - -import com.devskiller.friendly_id.FriendlyId; +import tools.jackson.databind.json.JsonMapper; +import org.junit.jupiter.api.Test; import static com.devskiller.friendly_id.spring.ObjectMapperConfiguration.mapper; import static org.assertj.core.api.Assertions.assertThat; -public class FieldWithoutFriendlyIdTest { +class FieldWithoutFriendlyIdTest { - private UUID uuid = UUID.fromString("f088ce5b-9279-4cc3-946a-c15ad740dd6d"); - private ObjectMapper mapper = mapper(); + private final UUID uuid = UUID.fromString("f088ce5b-9279-4cc3-946a-c15ad740dd6d"); + private JsonMapper jsonMapper = mapper(); @Test - public void shouldAllowToDoNotCodeUuidInDataObject() throws Exception { - Foo foo = new Foo(); - foo.setRawUuid(uuid); - foo.setFriendlyId(uuid); + void shouldAllowToDoNotCodeUuidInDataObject() { + var foo = new Foo(uuid, uuid); - String json = mapper.writeValueAsString(foo); + var json = jsonMapper.writeValueAsString(foo); - assertThat(json).isEqualToIgnoringWhitespace( - "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}" - ); + // JSON field order may vary, so check each field separately + assertThat(json).contains("\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\""); + assertThat(json).contains("\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\""); - Foo cloned = mapper.readValue(json, Foo.class); - assertThat(cloned.getRawUuid()).isEqualTo(foo.getFriendlyId()); + var cloned = jsonMapper.readValue(json, Foo.class); + assertThat(cloned.rawUuid()).isEqualTo(foo.friendlyId()); } @Test - public void shouldDeserializeUuidsInDataObject() throws Exception { - String json = "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}"; + void shouldDeserializeUuidsInDataObject() { + var json = """ + {"rawUuid":"f088ce5b-9279-4cc3-946a-c15ad740dd6d","friendlyId":"7Jsg6CPDscHawyJfE70b9x"}"""; - Foo cloned = mapper.readValue(json, Foo.class); - assertThat(cloned.getRawUuid()).isEqualTo(uuid); - assertThat(cloned.getFriendlyId()).isEqualTo(uuid); + var cloned = jsonMapper.readValue(json, Foo.class); + assertThat(cloned.rawUuid()).isEqualTo(uuid); + assertThat(cloned.friendlyId()).isEqualTo(uuid); } - @Test - public void shouldSerializeUuidsInValueObject() throws Exception { - mapper = mapper(new ParameterNamesModule()); + void shouldSerializeUuidsInValueObject() { + jsonMapper = mapper(); - Bar bar = new Bar(uuid, uuid); + var bar = new Bar(uuid, uuid); - String json = mapper.writeValueAsString(bar); + var json = jsonMapper.writeValueAsString(bar); - assertThat(json).isEqualToIgnoringWhitespace( - "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}" - ); + assertThat(json).isEqualToIgnoringWhitespace(""" + {"rawUuid":"f088ce5b-9279-4cc3-946a-c15ad740dd6d","friendlyId":"7Jsg6CPDscHawyJfE70b9x"}"""); } @Test - public void shouldDeserializeUuuidsValueObject() throws Exception { - mapper = mapper(new ParameterNamesModule()); + void shouldDeserializeUuidsInValueObject() { + jsonMapper = mapper(); - String json = "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}"; + var json = """ + {"rawUuid":"f088ce5b-9279-4cc3-946a-c15ad740dd6d","friendlyId":"7Jsg6CPDscHawyJfE70b9x"}"""; - Bar deserialized = mapper.readValue(json, Bar.class); + var deserialized = jsonMapper.readValue(json, Bar.class); - assertThat(deserialized.getRawUuid()).isEqualTo(uuid); - assertThat(deserialized.getFriendlyId()).isEqualTo(uuid); + assertThat(deserialized.rawUuid()).isEqualTo(uuid); + assertThat(deserialized.friendlyId()).isEqualTo(uuid); } - } diff --git a/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/Foo.java b/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/Foo.java index 8c96109..cd96f4c 100644 --- a/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/Foo.java +++ b/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/Foo.java @@ -2,29 +2,10 @@ import java.util.UUID; -import com.devskiller.friendly_id.jackson.FriendlyIdFormat; -import com.devskiller.friendly_id.jackson.IdFormat; +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; -public class Foo { - - @IdFormat(FriendlyIdFormat.RAW) - private UUID rawUuid; - - private UUID friendlyId; - - public UUID getRawUuid() { - return rawUuid; - } - - public void setRawUuid(UUID rawUuid) { - this.rawUuid = rawUuid; - } - - public UUID getFriendlyId() { - return friendlyId; - } - - public void setFriendlyId(UUID friendlyId) { - this.friendlyId = friendlyId; - } -} +public record Foo( + @IdFormat(FriendlyIdFormat.RAW) UUID rawUuid, + UUID friendlyId +) {} diff --git a/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/FriendlyIdDeserializerTest.java b/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/FriendlyIdDeserializerTest.java index 822f25b..24c2741 100644 --- a/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/FriendlyIdDeserializerTest.java +++ b/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/FriendlyIdDeserializerTest.java @@ -2,17 +2,17 @@ import java.util.UUID; -import org.junit.Test; +import org.junit.jupiter.api.Test; import com.devskiller.friendly_id.FriendlyId; import static com.devskiller.friendly_id.spring.ObjectMapperConfiguration.mapper; import static org.assertj.core.api.Assertions.assertThat; -public class FriendlyIdDeserializerTest { +class FriendlyIdDeserializerTest { @Test - public void shouldSerializeFriendlyId() throws Exception { + void shouldSerializeFriendlyId() { UUID uuid = UUID.randomUUID(); String json = mapper().writeValueAsString(uuid); System.out.println(json); @@ -20,7 +20,7 @@ public void shouldSerializeFriendlyId() throws Exception { } @Test - public void shouldDeserializeFriendlyId() throws Exception { + void shouldDeserializeFriendlyId() { String friendlyId = "2YSfgVHnEYbYgfFKhEX3Sz"; UUID uuid = mapper().readValue("\"" + friendlyId + "\"", UUID.class); assertThat(uuid).isEqualByComparingTo(FriendlyId.toUuid(friendlyId)); diff --git a/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/ObjectMapperConfiguration.java b/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/ObjectMapperConfiguration.java index e200068..978c9a4 100644 --- a/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/ObjectMapperConfiguration.java +++ b/friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/ObjectMapperConfiguration.java @@ -1,16 +1,18 @@ package com.devskiller.friendly_id.spring; -import com.fasterxml.jackson.databind.Module; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.json.JsonMapper; import com.devskiller.friendly_id.jackson.FriendlyIdModule; public class ObjectMapperConfiguration { - protected static ObjectMapper mapper(Module... modules) { - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new FriendlyIdModule()); - mapper.registerModules(modules); - return mapper; + protected static JsonMapper mapper(JacksonModule... modules) { + JsonMapper.Builder builder = JsonMapper.builder() + .addModule(new FriendlyIdModule()); + for (JacksonModule module : modules) { + builder.addModule(module); + } + return builder.build(); } } diff --git a/friendly-id-jackson2-datatype/pom.xml b/friendly-id-jackson2-datatype/pom.xml new file mode 100644 index 0000000..15c589b --- /dev/null +++ b/friendly-id-jackson2-datatype/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + + friendly-id-project + com.devskiller.friendly-id + ${revision} + .. + + + friendly-id-jackson2-datatype + + FriendlyId Jackson 2.x Datatype + Jackson 2.x module for JSON serialization/deserialization of UUIDs as FriendlyIds + + + 2.18.2 + + + + + com.devskiller.friendly-id + friendly-id + ${project.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson2.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson2.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson2.version} + + + com.fasterxml.jackson.module + jackson-module-parameter-names + ${jackson2.version} + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + + + diff --git a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdAnnotationIntrospector.java similarity index 64% rename from friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java rename to friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdAnnotationIntrospector.java index 3f82115..3da47d3 100644 --- a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdAnnotationIntrospector.java @@ -1,7 +1,9 @@ -package com.devskiller.friendly_id.jackson; +package com.devskiller.friendly_id.jackson2; import java.util.UUID; +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; import com.fasterxml.jackson.databind.deser.std.UUIDDeserializer; import com.fasterxml.jackson.databind.introspect.Annotated; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; @@ -17,12 +19,10 @@ public Object findSerializer(Annotated annotatedMethod) { IdFormat annotation = _findAnnotation(annotatedMethod, IdFormat.class); if (annotatedMethod.getRawType() == UUID.class) { if (annotation != null) { - switch (annotation.value()) { - case RAW: - return UUIDSerializer.class; - case URL62: - return FriendlyIdSerializer.class; - } + return switch (annotation.value()) { + case RAW -> UUIDSerializer.class; + case URL62 -> FriendlyIdSerializer.class; + }; } return FriendlyIdSerializer.class; } else { @@ -32,28 +32,22 @@ public Object findSerializer(Annotated annotatedMethod) { @Override public Object findDeserializer(Annotated annotatedMethod) { - IdFormat annotation = _findAnnotation(annotatedMethod, IdFormat.class); + var annotation = _findAnnotation(annotatedMethod, IdFormat.class); if (rawDeserializationType(annotatedMethod) == UUID.class) { if (annotation != null) { - switch (annotation.value()) { - case RAW: - return UUIDDeserializer.class; - case URL62: - return FriendlyIdDeserializer.class; - } + return switch (annotation.value()) { + case RAW -> UUIDDeserializer.class; + case URL62 -> FriendlyIdDeserializer.class; + }; } return FriendlyIdDeserializer.class; - } else { - return null; } + return null; } private Class rawDeserializationType(Annotated a) { - if (a instanceof AnnotatedMethod) { - AnnotatedMethod am = (AnnotatedMethod) a; - if (am.getParameterCount() == 1) { - return am.getRawParameterType(0); - } + if (a instanceof AnnotatedMethod am && am.getParameterCount() == 1) { + return am.getRawParameterType(0); } return a.getRawType(); } diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdDeserializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdDeserializer.java new file mode 100644 index 0000000..e013098 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdDeserializer.java @@ -0,0 +1,33 @@ +package com.devskiller.friendly_id.jackson2; + +import java.io.IOException; +import java.util.UUID; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.UUIDDeserializer; + +import com.devskiller.friendly_id.FriendlyId; + +public class FriendlyIdDeserializer extends UUIDDeserializer { + + @Override + public UUID deserialize(JsonParser parser, DeserializationContext deserializationContext) throws IOException { + + JsonToken token = parser.getCurrentToken(); + if (token == JsonToken.VALUE_STRING) { + String string = parser.getValueAsString().trim(); + if (looksLikeUuid(string)) { + return super.deserialize(parser, deserializationContext); + } else { + return FriendlyId.toUuid(string); + } + } + throw new IllegalStateException("This is not friendly id"); + } + + private boolean looksLikeUuid(String value) { + return value.contains("-"); + } +} diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdJackson2Module.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdJackson2Module.java new file mode 100644 index 0000000..3e9fc3b --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdJackson2Module.java @@ -0,0 +1,27 @@ +package com.devskiller.friendly_id.jackson2; + +import java.util.UUID; + +import com.devskiller.friendly_id.type.FriendlyId; +import com.fasterxml.jackson.databind.module.SimpleModule; + +public class FriendlyIdJackson2Module extends SimpleModule { + + private final FriendlyIdAnnotationIntrospector introspector; + + public FriendlyIdJackson2Module() { + introspector = new FriendlyIdAnnotationIntrospector(); + addDeserializer(UUID.class, new FriendlyIdDeserializer()); + addSerializer(UUID.class, new FriendlyIdSerializer()); + + // Add serializer/deserializer for FriendlyId value object + addDeserializer(FriendlyId.class, new FriendlyIdValueDeserializer()); + addSerializer(FriendlyId.class, new FriendlyIdValueSerializer()); + } + + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + context.insertAnnotationIntrospector(introspector); + } +} diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdSerializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdSerializer.java new file mode 100644 index 0000000..87b3012 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdSerializer.java @@ -0,0 +1,22 @@ +package com.devskiller.friendly_id.jackson2; + +import java.io.IOException; +import java.util.UUID; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import com.devskiller.friendly_id.FriendlyId; + +public class FriendlyIdSerializer extends StdSerializer { + + public FriendlyIdSerializer() { + super(UUID.class); + } + + @Override + public void serialize(UUID uuid, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeString(FriendlyId.toFriendlyId(uuid)); + } +} diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueDeserializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueDeserializer.java new file mode 100644 index 0000000..d352651 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueDeserializer.java @@ -0,0 +1,22 @@ +package com.devskiller.friendly_id.jackson2; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * JSON deserializer for {@link FriendlyId} value object. + * Deserializes JSON strings to FriendlyId instances. + */ +public class FriendlyIdValueDeserializer extends JsonDeserializer { + + @Override + public FriendlyId deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String friendlyIdString = p.getValueAsString(); + return FriendlyId.fromString(friendlyIdString); + } +} diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueSerializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueSerializer.java new file mode 100644 index 0000000..0f764cf --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueSerializer.java @@ -0,0 +1,21 @@ +package com.devskiller.friendly_id.jackson2; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * JSON serializer for {@link FriendlyId} value object. + * Serializes FriendlyId instances as their string representation. + */ +public class FriendlyIdValueSerializer extends JsonSerializer { + + @Override + public void serialize(FriendlyId value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(value.toString()); + } +} diff --git a/friendly-id-jackson2-datatype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/friendly-id-jackson2-datatype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module new file mode 100644 index 0000000..de36ea4 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module @@ -0,0 +1 @@ +com.devskiller.friendly_id.jackson2.FriendlyIdJackson2Module diff --git a/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/Bar.java b/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/Bar.java new file mode 100644 index 0000000..6d87496 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/Bar.java @@ -0,0 +1,29 @@ +package com.devskiller.friendly_id.spring; + +import java.util.UUID; + +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; + +public class Bar { + + @IdFormat(FriendlyIdFormat.RAW) + private final UUID rawUuid; + + private final UUID friendlyId; + + public Bar(UUID rawUuid, UUID friendlyId) { + this.rawUuid = rawUuid; + this.friendlyId = friendlyId; + } + + public UUID getRawUuid() { + return rawUuid; + } + + public UUID getFriendlyId() { + return friendlyId; + } + + +} diff --git a/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/FieldWithoutFriendlyIdTest.java b/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/FieldWithoutFriendlyIdTest.java new file mode 100644 index 0000000..aa99b6d --- /dev/null +++ b/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/FieldWithoutFriendlyIdTest.java @@ -0,0 +1,70 @@ +package com.devskiller.friendly_id.spring; + +import java.util.UUID; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import org.junit.jupiter.api.Test; + +import com.devskiller.friendly_id.FriendlyId; + +import static com.devskiller.friendly_id.spring.ObjectMapperConfiguration.mapper; +import static org.assertj.core.api.Assertions.assertThat; + +class FieldWithoutFriendlyIdTest { + + private final UUID uuid = UUID.fromString("f088ce5b-9279-4cc3-946a-c15ad740dd6d"); + private ObjectMapper mapper = mapper(); + + @Test + void shouldAllowToDoNotCodeUuidInDataObject() throws Exception { + Foo foo = new Foo(); + foo.setRawUuid(uuid); + foo.setFriendlyId(uuid); + + String json = mapper.writeValueAsString(foo); + + assertThat(json).isEqualToIgnoringWhitespace( + "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}" + ); + + Foo cloned = mapper.readValue(json, Foo.class); + assertThat(cloned.getRawUuid()).isEqualTo(foo.getFriendlyId()); + } + + @Test + void shouldDeserializeUuidsInDataObject() throws Exception { + String json = "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}"; + + Foo cloned = mapper.readValue(json, Foo.class); + assertThat(cloned.getRawUuid()).isEqualTo(uuid); + assertThat(cloned.getFriendlyId()).isEqualTo(uuid); + } + + + @Test + void shouldSerializeUuidsInValueObject() throws Exception { + mapper = mapper(new ParameterNamesModule()); + + Bar bar = new Bar(uuid, uuid); + + String json = mapper.writeValueAsString(bar); + + assertThat(json).isEqualToIgnoringWhitespace( + "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}" + ); + } + + @Test + void shouldDeserializeUuuidsValueObject() throws Exception { + mapper = mapper(new ParameterNamesModule()); + + String json = "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}"; + + Bar deserialized = mapper.readValue(json, Bar.class); + + assertThat(deserialized.getRawUuid()).isEqualTo(uuid); + assertThat(deserialized.getFriendlyId()).isEqualTo(uuid); + } + +} diff --git a/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/Foo.java b/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/Foo.java new file mode 100644 index 0000000..c866ed3 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/Foo.java @@ -0,0 +1,30 @@ +package com.devskiller.friendly_id.spring; + +import java.util.UUID; + +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; + +public class Foo { + + @IdFormat(FriendlyIdFormat.RAW) + private UUID rawUuid; + + private UUID friendlyId; + + public UUID getRawUuid() { + return rawUuid; + } + + public void setRawUuid(UUID rawUuid) { + this.rawUuid = rawUuid; + } + + public UUID getFriendlyId() { + return friendlyId; + } + + public void setFriendlyId(UUID friendlyId) { + this.friendlyId = friendlyId; + } +} diff --git a/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/FriendlyIdDeserializerTest.java b/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/FriendlyIdDeserializerTest.java new file mode 100644 index 0000000..e0525b3 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/FriendlyIdDeserializerTest.java @@ -0,0 +1,28 @@ +package com.devskiller.friendly_id.spring; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.devskiller.friendly_id.FriendlyId; + +import static com.devskiller.friendly_id.spring.ObjectMapperConfiguration.mapper; +import static org.assertj.core.api.Assertions.assertThat; + +class FriendlyIdDeserializerTest { + + @Test + void shouldSerializeFriendlyId() throws Exception { + UUID uuid = UUID.randomUUID(); + String json = mapper().writeValueAsString(uuid); + System.out.println(json); + assertThat(json).contains(FriendlyId.toFriendlyId(uuid)); + } + + @Test + void shouldDeserializeFriendlyId() throws Exception { + String friendlyId = "2YSfgVHnEYbYgfFKhEX3Sz"; + UUID uuid = mapper().readValue("\"" + friendlyId + "\"", UUID.class); + assertThat(uuid).isEqualByComparingTo(FriendlyId.toUuid(friendlyId)); + } +} diff --git a/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/ObjectMapperConfiguration.java b/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/ObjectMapperConfiguration.java new file mode 100644 index 0000000..6149e61 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/ObjectMapperConfiguration.java @@ -0,0 +1,16 @@ +package com.devskiller.friendly_id.spring; + +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.devskiller.friendly_id.jackson2.FriendlyIdJackson2Module; + +public class ObjectMapperConfiguration { + + protected static ObjectMapper mapper(Module... modules) { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new FriendlyIdJackson2Module()); + mapper.registerModules(modules); + return mapper; + } +} diff --git a/friendly-id-jooq/README.md b/friendly-id-jooq/README.md new file mode 100644 index 0000000..f3d90b7 --- /dev/null +++ b/friendly-id-jooq/README.md @@ -0,0 +1,99 @@ +# FriendlyId jOOQ Integration + +jOOQ converter for transparent UUID to FriendlyId conversion in database queries. + +## Overview + +This module provides a jOOQ `Converter` that allows you to work with FriendlyId value objects in your Java code while storing UUIDs in the database. jOOQ will automatically handle the conversion between the two representations. + +The `FriendlyId` value object is **memory-efficient**, storing the UUID internally (16 bytes) and computing the FriendlyId string representation only when needed (e.g., `toString()`). This is more efficient than storing String representations (~40-50 bytes). + +## Maven Dependency + +```xml + + com.devskiller.friendly-id + friendly-id-jooq + 1.1.1-SNAPSHOT + +``` + +## Usage with jOOQ Code Generator + +Configure the converter in your jOOQ code generation configuration to apply it to specific columns or all UUID columns: + +```xml + + + + + + com.devskiller.friendly_id.type.FriendlyId + com.devskiller.friendly_id.jooq.FriendlyIdConverter + .*\.ID + UUID + + + + + +``` + +## Example + +```java +import com.devskiller.friendly_id.type.FriendlyId; + +// Query using FriendlyId +FriendlyId friendlyId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); +UserRecord user = create + .selectFrom(USER) + .where(USER.ID.eq(friendlyId)) // Automatically converted to UUID for database + .fetchOne(); + +// Get FriendlyId from result +FriendlyId userId = user.getId(); // Returns FriendlyId value object +String userIdString = userId.toString(); // Get string representation when needed + +// Insert with FriendlyId +create.insertInto(USER) + .set(USER.ID, FriendlyId.random()) + .set(USER.NAME, "John Doe") + .execute(); + +// FriendlyId prints nicely +System.out.println("User ID: " + userId); // Prints: User ID: 5wbwf6yUxVBcr48AMbz9cb +``` + +## How It Works + +The `FriendlyIdConverter` implements `org.jooq.Converter`: + +- **Database Type (fromType)**: `UUID` - the actual column type in your database (16 bytes) +- **User Type (toType)**: `FriendlyId` - value object wrapping UUID in your Java code (~28 bytes) +- **Conversion**: Bidirectional conversion between UUID and FriendlyId value objects + +## Memory Efficiency + +| Type | Memory Usage | Notes | +|------|-------------|-------| +| UUID | 16 bytes | Database storage | +| FriendlyId | ~28 bytes | 16 bytes UUID + ~12 bytes object header | +| String | ~40-50 bytes | FriendlyId as String (previous approach) | + +**Result**: ~30-40% memory savings compared to storing FriendlyId as String + +## Benefits + +- **Memory Efficient**: Store UUIDs internally, compute strings only when needed +- **URL-Friendly**: Automatic conversion to human-readable, Base62-encoded IDs +- **Type Safety**: Strong typing prevents mixing UUIDs with FriendlyIds +- **Database Efficiency**: Store compact UUIDs in the database +- **Transparent**: No manual conversion needed in your application code +- **Pretty Printing**: Automatic FriendlyId string representation via `toString()` + +## See Also + +- [jOOQ Converters Documentation](https://www.jooq.org/doc/latest/manual/code-generation/codegen-advanced/codegen-config-database/codegen-database-forced-types/codegen-database-forced-types-converter/) +- [FriendlyId Core Library](../friendly-id/) +- [FriendlyId Value Object](../friendly-id/src/main/java/com/devskiller/friendly_id/type/FriendlyId.java) diff --git a/friendly-id-jooq/pom.xml b/friendly-id-jooq/pom.xml new file mode 100644 index 0000000..7696bf3 --- /dev/null +++ b/friendly-id-jooq/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + friendly-id-project + com.devskiller.friendly-id + ${revision} + .. + + + friendly-id-jooq + + FriendlyId jOOQ Integration + jOOQ converters for FriendlyId - enables transparent UUID to FriendlyId conversion in database queries + + + + com.devskiller.friendly-id + friendly-id + ${project.version} + + + org.jooq + jooq + provided + + + + + org.junit.jupiter + junit-jupiter + test + + + diff --git a/friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/FriendlyIdConverter.java b/friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/FriendlyIdConverter.java new file mode 100644 index 0000000..ad954ac --- /dev/null +++ b/friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/FriendlyIdConverter.java @@ -0,0 +1,112 @@ +package com.devskiller.friendly_id.jooq; + +import java.util.UUID; + +import org.jooq.Converter; + +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * jOOQ converter for transparent UUID to FriendlyId conversion in database queries. + *

+ * This converter allows you to work with FriendlyId value objects in your Java code while + * storing UUIDs in the database. jOOQ will automatically handle the conversion between + * the two representations. + *

+ *

+ * The FriendlyId value object is memory-efficient, storing the UUID internally (16 bytes) + * and computing the FriendlyId string representation only when needed (e.g., toString()). + * This is more efficient than storing String representations (~40-50 bytes). + *

+ * + *

Usage with jOOQ Code Generator

+ *

+ * Configure this converter in your jOOQ code generation configuration to apply it + * to specific columns or all UUID columns: + *

+ *
{@code
+ * 
+ *   
+ *     
+ *       
+ *         
+ *           com.devskiller.friendly_id.type.FriendlyId
+ *           com.devskiller.friendly_id.jooq.FriendlyIdConverter
+ *           .*\.ID
+ *           UUID
+ *         
+ *       
+ *     
+ *   
+ * 
+ * }
+ * + *

Manual Usage Example

+ *
{@code
+ * // Query using FriendlyId
+ * FriendlyId friendlyId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb");
+ * UserRecord user = create
+ *     .selectFrom(USER)
+ *     .where(USER.ID.eq(friendlyId))
+ *     .fetchOne();
+ *
+ * // Get FriendlyId from result
+ * FriendlyId userId = user.getId(); // Returns FriendlyId value object
+ * String friendlyIdString = userId.toString(); // Get string when needed
+ *
+ * // Insert with FriendlyId
+ * create.insertInto(USER)
+ *     .set(USER.ID, FriendlyId.random())
+ *     .set(USER.NAME, "John Doe")
+ *     .execute();
+ * }
+ * + * @see FriendlyId + * @see org.jooq.Converter + */ +public class FriendlyIdConverter implements Converter { + + private static final long serialVersionUID = 1L; + + /** + * Converts a database UUID to a FriendlyId value object. + * + * @param databaseObject the UUID from the database, may be {@code null} + * @return the FriendlyId value object, or {@code null} if input is {@code null} + */ + @Override + public FriendlyId from(UUID databaseObject) { + return databaseObject == null ? null : FriendlyId.of(databaseObject); + } + + /** + * Converts a FriendlyId value object to a database UUID. + * + * @param userObject the FriendlyId value object, may be {@code null} + * @return the UUID representation, or {@code null} if input is {@code null} + */ + @Override + public UUID to(FriendlyId userObject) { + return userObject == null ? null : userObject.uuid(); + } + + /** + * Returns the database type (UUID). + * + * @return {@code UUID.class} + */ + @Override + public Class fromType() { + return UUID.class; + } + + /** + * Returns the user type (FriendlyId). + * + * @return {@code FriendlyId.class} + */ + @Override + public Class toType() { + return FriendlyId.class; + } +} diff --git a/friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/package-info.java b/friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/package-info.java new file mode 100644 index 0000000..3c15de1 --- /dev/null +++ b/friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/package-info.java @@ -0,0 +1,17 @@ +/** + * jOOQ integration for FriendlyId. + *

+ * This package provides jOOQ converters that enable transparent conversion between + * UUID database columns and FriendlyId strings in your Java code. + *

+ * + *

Usage

+ *

+ * Configure the {@link com.devskiller.friendly_id.jooq.FriendlyIdConverter} in your + * jOOQ code generation configuration to automatically apply FriendlyId conversion + * to UUID columns. + *

+ * + * @see com.devskiller.friendly_id.jooq.FriendlyIdConverter + */ +package com.devskiller.friendly_id.jooq; diff --git a/friendly-id-jooq/src/test/java/com/devskiller/friendly_id/jooq/FriendlyIdConverterTest.java b/friendly-id-jooq/src/test/java/com/devskiller/friendly_id/jooq/FriendlyIdConverterTest.java new file mode 100644 index 0000000..d5c533e --- /dev/null +++ b/friendly-id-jooq/src/test/java/com/devskiller/friendly_id/jooq/FriendlyIdConverterTest.java @@ -0,0 +1,100 @@ +package com.devskiller.friendly_id.jooq; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.devskiller.friendly_id.type.FriendlyId; + +import static org.junit.jupiter.api.Assertions.*; + +class FriendlyIdConverterTest { + + private final FriendlyIdConverter converter = new FriendlyIdConverter(); + + @Test + void shouldConvertUuidToFriendlyId() { + // given + UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); + + // when + FriendlyId friendlyId = converter.from(uuid); + + // then + assertEquals(uuid, friendlyId.uuid()); + } + + @Test + void shouldConvertFriendlyIdToUuid() { + // given + FriendlyId friendlyId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); + + // when + UUID uuid = converter.to(friendlyId); + + // then + assertEquals(friendlyId.uuid(), uuid); + } + + @Test + void shouldHandleNullUuid() { + // when + FriendlyId friendlyId = converter.from(null); + + // then + assertNull(friendlyId); + } + + @Test + void shouldHandleNullFriendlyId() { + // when + UUID uuid = converter.to(null); + + // then + assertNull(uuid); + } + + @Test + void shouldReturnCorrectFromType() { + // when + Class fromType = converter.fromType(); + + // then + assertEquals(UUID.class, fromType); + } + + @Test + void shouldReturnCorrectToType() { + // when + Class toType = converter.toType(); + + // then + assertEquals(FriendlyId.class, toType); + } + + @Test + void shouldBeReversible() { + // given + UUID originalUuid = UUID.randomUUID(); + + // when + FriendlyId friendlyId = converter.from(originalUuid); + UUID convertedUuid = converter.to(friendlyId); + + // then + assertEquals(originalUuid, convertedUuid); + } + + @Test + void shouldConvertToStringWhenNeeded() { + // given + UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); + FriendlyId friendlyId = converter.from(uuid); + + // when + String friendlyIdString = friendlyId.toString(); + + // then + assertEquals(com.devskiller.friendly_id.FriendlyId.toFriendlyId(uuid), friendlyIdString); + } +} diff --git a/friendly-id-jpa/README.md b/friendly-id-jpa/README.md new file mode 100644 index 0000000..443ae23 --- /dev/null +++ b/friendly-id-jpa/README.md @@ -0,0 +1,180 @@ +# FriendlyId JPA Integration + +JPA AttributeConverter for transparent UUID to FriendlyId conversion in entity mappings. + +## Overview + +This module provides a JPA `AttributeConverter` that allows you to work with FriendlyId value objects in your JPA entities while storing UUIDs in the database. JPA will automatically handle the conversion between the two representations. + +The `FriendlyId` value object is **memory-efficient**, storing the UUID internally (16 bytes) and computing the FriendlyId string representation only when needed (e.g., `toString()`). This is more efficient than storing String representations (~40-50 bytes). + +## Maven Dependency + +```xml + + com.devskiller.friendly-id + friendly-id-jpa + 1.1.1-SNAPSHOT + +``` + +## Automatic Usage (Recommended) + +The converter is **automatically applied** to all FriendlyId attributes thanks to `@Converter(autoApply = true)`. No configuration needed! + +```java +import com.devskiller.friendly_id.type.FriendlyId; +import jakarta.persistence.*; + +@Entity +@Table(name = "users") +public class User { + + @Id + private FriendlyId id; // Automatically converted to/from UUID + + private String name; + + // getters/setters +} +``` + +## Usage Examples + +### Creating Entities + +```java +// Create with random FriendlyId +User user = new User(); +user.setId(FriendlyId.random()); +user.setName("John Doe"); +em.persist(user); + +// Create from FriendlyId string +User user = new User(); +user.setId(FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb")); +user.setName("Jane Doe"); +em.persist(user); +``` + +### Querying + +```java +// JPQL query +FriendlyId userId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); +User user = em.createQuery("SELECT u FROM User u WHERE u.id = :id", User.class) + .setParameter("id", userId) + .getSingleResult(); + +// Find by ID +User user = em.find(User.class, FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb")); + +// Criteria API +CriteriaBuilder cb = em.getCriteriaBuilder(); +CriteriaQuery query = cb.createQuery(User.class); +Root root = query.from(User.class); +query.where(cb.equal(root.get("id"), userId)); +List users = em.createQuery(query).getResultList(); +``` + +### Native Queries + +For native queries, use the UUID directly: + +```java +FriendlyId userId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); +User user = em.createNativeQuery( + "SELECT * FROM users WHERE id = ?", + User.class) + .setParameter(1, userId.uuid()) // Use .uuid() for native queries + .getSingleResult(); +``` + +### Pretty Printing + +```java +User user = em.find(User.class, someId); +System.out.println("User ID: " + user.getId()); +// Prints: User ID: 5wbwf6yUxVBcr48AMbz9cb +``` + +## Manual Application (Optional) + +If you disabled autoApply or need explicit control: + +```java +@Entity +public class User { + @Id + @Convert(converter = FriendlyIdConverter.class) + private FriendlyId id; + + private String name; +} +``` + +## Spring Data JPA Example + +```java +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + + // Derived query methods work automatically + Optional findByName(String name); + + // Custom queries with FriendlyId parameters + @Query("SELECT u FROM User u WHERE u.id = :id") + Optional findByFriendlyId(@Param("id") FriendlyId id); +} + +// Usage +FriendlyId id = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); +Optional user = userRepository.findById(id); +``` + +## How It Works + +The `FriendlyIdConverter` implements `jakarta.persistence.AttributeConverter`: + +- **Database Column Type**: `UUID` (16 bytes) +- **Entity Attribute Type**: `FriendlyId` value object (~28 bytes in memory) +- **Conversion**: Bidirectional and automatic + +## Memory Efficiency + +| Type | Memory Usage | Notes | +|------|-------------|-------| +| UUID | 16 bytes | Database storage | +| FriendlyId | ~28 bytes | 16 bytes UUID + ~12 bytes object header | +| String | ~40-50 bytes | FriendlyId as String (avoided) | + +**Result**: ~30-40% memory savings compared to storing FriendlyId as String + +## Benefits + +- **Zero Configuration**: Works automatically with `autoApply = true` +- **Memory Efficient**: Store UUIDs internally, compute strings only when needed +- **Type Safety**: Strong typing prevents mixing UUIDs with FriendlyIds +- **Database Efficiency**: Store compact UUIDs in the database +- **Transparent**: No manual conversion in entity code +- **Pretty Printing**: Automatic FriendlyId string representation via `toString()` +- **Spring Data Compatible**: Works seamlessly with Spring Data JPA repositories + +## Database Schema + +The database column should be UUID type: + +```sql +CREATE TABLE users ( + id UUID PRIMARY KEY, + name VARCHAR(255) +); +``` + +## See Also + +- [JPA AttributeConverter Documentation](https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/attributeconverter) +- [FriendlyId Core Library](../friendly-id/) +- [FriendlyId Value Object](../friendly-id/src/main/java/com/devskiller/friendly_id/type/FriendlyId.java) +- [FriendlyId jOOQ Integration](../friendly-id-jooq/) diff --git a/friendly-id-jpa/pom.xml b/friendly-id-jpa/pom.xml new file mode 100644 index 0000000..f079101 --- /dev/null +++ b/friendly-id-jpa/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + + friendly-id-project + com.devskiller.friendly-id + ${revision} + .. + + + friendly-id-jpa + + FriendlyId JPA Integration + JPA AttributeConverter for FriendlyId - enables transparent UUID to FriendlyId conversion in JPA entities + + + + com.devskiller.friendly-id + friendly-id + ${project.version} + + + jakarta.persistence + jakarta.persistence-api + 3.1.0 + provided + + + + + org.junit.jupiter + junit-jupiter + test + + + diff --git a/friendly-id-jpa/src/main/java/com/devskiller/friendly_id/jpa/FriendlyIdConverter.java b/friendly-id-jpa/src/main/java/com/devskiller/friendly_id/jpa/FriendlyIdConverter.java new file mode 100644 index 0000000..760534a --- /dev/null +++ b/friendly-id-jpa/src/main/java/com/devskiller/friendly_id/jpa/FriendlyIdConverter.java @@ -0,0 +1,105 @@ +package com.devskiller.friendly_id.jpa; + +import java.util.UUID; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * JPA AttributeConverter for transparent UUID to FriendlyId conversion in entity mappings. + *

+ * This converter allows you to work with FriendlyId value objects in your JPA entities while + * storing UUIDs in the database. JPA will automatically handle the conversion between + * the two representations. + *

+ *

+ * The FriendlyId value object is memory-efficient, storing the UUID internally (16 bytes) + * and computing the FriendlyId string representation only when needed (e.g., toString()). + * This is more efficient than storing String representations (~40-50 bytes). + *

+ * + *

Automatic Registration (JPA 2.1+)

+ *

+ * The converter is automatically applied to all FriendlyId attributes thanks to the + * {@code @Converter(autoApply = true)} annotation. No additional configuration needed. + *

+ * + *

Usage in Entity

+ *
{@code
+ * @Entity
+ * public class User {
+ *     @Id
+ *     private FriendlyId id;
+ *
+ *     private String name;
+ *
+ *     // getters/setters
+ * }
+ * }
+ * + *

Query Examples

+ *
{@code
+ * // JPQL - use UUID parameter
+ * FriendlyId userId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb");
+ * User user = em.createQuery("SELECT u FROM User u WHERE u.id = :id", User.class)
+ *     .setParameter("id", userId)
+ *     .getSingleResult();
+ *
+ * // Criteria API
+ * CriteriaBuilder cb = em.getCriteriaBuilder();
+ * CriteriaQuery query = cb.createQuery(User.class);
+ * Root root = query.from(User.class);
+ * query.where(cb.equal(root.get("id"), userId));
+ *
+ * // Native query - use UUID
+ * User user = em.createNativeQuery(
+ *         "SELECT * FROM users WHERE id = ?",
+ *         User.class)
+ *     .setParameter(1, userId.uuid())
+ *     .getSingleResult();
+ * }
+ * + *

Manual Application (Optional)

+ *

+ * If autoApply is disabled or you need explicit control: + *

+ *
{@code
+ * @Entity
+ * public class User {
+ *     @Id
+ *     @Convert(converter = FriendlyIdConverter.class)
+ *     private FriendlyId id;
+ * }
+ * }
+ * + * @see FriendlyId + * @see jakarta.persistence.AttributeConverter + * @since 1.1.1 + */ +@Converter(autoApply = true) +public class FriendlyIdConverter implements AttributeConverter { + + /** + * Converts a FriendlyId value object to a database UUID. + * + * @param attribute the FriendlyId value object, may be {@code null} + * @return the UUID for database storage, or {@code null} if input is {@code null} + */ + @Override + public UUID convertToDatabaseColumn(FriendlyId attribute) { + return attribute == null ? null : attribute.uuid(); + } + + /** + * Converts a database UUID to a FriendlyId value object. + * + * @param dbData the UUID from the database, may be {@code null} + * @return the FriendlyId value object, or {@code null} if input is {@code null} + */ + @Override + public FriendlyId convertToEntityAttribute(UUID dbData) { + return dbData == null ? null : FriendlyId.of(dbData); + } +} diff --git a/friendly-id-jpa/src/main/java/com/devskiller/friendly_id/jpa/package-info.java b/friendly-id-jpa/src/main/java/com/devskiller/friendly_id/jpa/package-info.java new file mode 100644 index 0000000..00d2e5c --- /dev/null +++ b/friendly-id-jpa/src/main/java/com/devskiller/friendly_id/jpa/package-info.java @@ -0,0 +1,15 @@ +/** + * JPA integration for FriendlyId. + *

+ * This package provides JPA AttributeConverter for automatic conversion between + * FriendlyId value objects and UUID database columns. + *

+ *

+ * The converter is automatically applied to all FriendlyId attributes in JPA entities + * thanks to {@code @Converter(autoApply = true)}. + *

+ * + * @see com.devskiller.friendly_id.jpa.FriendlyIdConverter + * @since 1.1.1 + */ +package com.devskiller.friendly_id.jpa; diff --git a/friendly-id-jpa/src/test/java/com/devskiller/friendly_id/jpa/FriendlyIdConverterTest.java b/friendly-id-jpa/src/test/java/com/devskiller/friendly_id/jpa/FriendlyIdConverterTest.java new file mode 100644 index 0000000..3c02edd --- /dev/null +++ b/friendly-id-jpa/src/test/java/com/devskiller/friendly_id/jpa/FriendlyIdConverterTest.java @@ -0,0 +1,82 @@ +package com.devskiller.friendly_id.jpa; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.devskiller.friendly_id.type.FriendlyId; + +import static org.junit.jupiter.api.Assertions.*; + +class FriendlyIdConverterTest { + + private final FriendlyIdConverter converter = new FriendlyIdConverter(); + + @Test + void shouldConvertFriendlyIdToUuid() { + // given + FriendlyId friendlyId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); + + // when + UUID uuid = converter.convertToDatabaseColumn(friendlyId); + + // then + assertEquals(friendlyId.uuid(), uuid); + } + + @Test + void shouldConvertUuidToFriendlyId() { + // given + UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); + + // when + FriendlyId friendlyId = converter.convertToEntityAttribute(uuid); + + // then + assertEquals(uuid, friendlyId.uuid()); + } + + @Test + void shouldHandleNullFriendlyId() { + // when + UUID uuid = converter.convertToDatabaseColumn(null); + + // then + assertNull(uuid); + } + + @Test + void shouldHandleNullUuid() { + // when + FriendlyId friendlyId = converter.convertToEntityAttribute(null); + + // then + assertNull(friendlyId); + } + + @Test + void shouldBeReversible() { + // given + UUID originalUuid = UUID.randomUUID(); + + // when + FriendlyId friendlyId = converter.convertToEntityAttribute(originalUuid); + UUID convertedUuid = converter.convertToDatabaseColumn(friendlyId); + + // then + assertEquals(originalUuid, convertedUuid); + } + + @Test + void shouldConvertToStringWhenNeeded() { + // given + UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); + FriendlyId friendlyId = converter.convertToEntityAttribute(uuid); + + // when + String friendlyIdString = friendlyId.toString(); + + // then + assertEquals(com.devskiller.friendly_id.FriendlyId.toFriendlyId(uuid), friendlyIdString); + } +} diff --git a/friendly-id-openfeign/pom.xml b/friendly-id-openfeign/pom.xml new file mode 100644 index 0000000..3650707 --- /dev/null +++ b/friendly-id-openfeign/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + friendly-id-project + com.devskiller.friendly-id + ${revision} + .. + + + friendly-id-openfeign + + FriendlyId OpenFeign Integration + OpenFeign client integration for FriendlyId - enables automatic FriendlyId encoding/decoding in Feign clients + + + + com.devskiller.friendly-id + friendly-id + ${project.version} + + + com.devskiller.friendly-id + friendly-id-jackson-datatype + ${project.version} + + + org.springframework.cloud + spring-cloud-starter-openfeign + provided + + + org.springframework.boot + spring-boot-starter-web + provided + + + + org.springframework.boot + spring-boot-starter-classic + provided + + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + diff --git a/friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdConfiguration.java b/friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdConfiguration.java new file mode 100644 index 0000000..84da235 --- /dev/null +++ b/friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdConfiguration.java @@ -0,0 +1,64 @@ +package com.devskiller.friendly_id.openfeign; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters; +import org.springframework.cloud.openfeign.support.SpringDecoder; +import org.springframework.cloud.openfeign.support.SpringEncoder; +import org.springframework.context.annotation.Bean; + +import feign.codec.Decoder; +import feign.codec.Encoder; + +/** + * Configuration for FriendlyId integration with Spring Cloud OpenFeign. + *

+ * This configuration can be used with {@code @FeignClient} to enable FriendlyId support: + *

+ *
{@code
+ * @FeignClient(name = "user-service", configuration = FriendlyIdConfiguration.class)
+ * public interface UserClient {
+ *
+ *     @GetMapping("/users/{id}")
+ *     UserDto getUser(@PathVariable UUID id);
+ *
+ *     @GetMapping("/users/{id}/profile")
+ *     ProfileDto getProfile(@PathVariable FriendlyId id);
+ * }
+ * }
+ *

+ * The configuration registers custom encoder and decoder that: + *

+ *
    + *
  • Convert UUID/FriendlyId to FriendlyId strings in request URLs/bodies
  • + *
  • Convert FriendlyId strings back to UUID/FriendlyId in responses
  • + *
+ * + * @since 1.1.1 + */ +@SuppressWarnings("deprecation") +public class FriendlyIdConfiguration { + + /** + * Creates a FriendlyId-aware Feign encoder. + * The encoder delegates to SpringEncoder for actual encoding but intercepts + * UUID and FriendlyId objects to convert them to FriendlyId strings. + */ + @Bean + @SuppressWarnings({"unchecked", "rawtypes"}) + public Encoder feignEncoder(ObjectFactory messageConverters) { + // Cast needed due to API compatibility between Spring Boot 4 and Spring Cloud OpenFeign + return new FriendlyIdEncoder(new SpringEncoder((ObjectFactory) messageConverters)); + } + + /** + * Creates a FriendlyId-aware Feign decoder. + * The decoder delegates to SpringDecoder for actual decoding but converts + * FriendlyId strings back to UUID or FriendlyId objects when needed. + */ + @Bean + @SuppressWarnings({"unchecked", "rawtypes"}) + public Decoder feignDecoder(ObjectFactory messageConverters) { + // Cast needed due to API compatibility between Spring Boot 4 and Spring Cloud OpenFeign + return new FriendlyIdDecoder(new SpringDecoder((ObjectFactory) messageConverters)); + } +} diff --git a/friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdDecoder.java b/friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdDecoder.java new file mode 100644 index 0000000..77b56ac --- /dev/null +++ b/friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdDecoder.java @@ -0,0 +1,53 @@ +package com.devskiller.friendly_id.openfeign; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.UUID; + +import com.devskiller.friendly_id.FriendlyId; + +import feign.FeignException; +import feign.Response; +import feign.codec.DecodeException; +import feign.codec.Decoder; + +/** + * Feign decoder that converts FriendlyId strings to UUID and FriendlyId objects in responses. + *

+ * This decoder wraps the default decoder and intercepts String responses that should be + * converted to UUID or FriendlyId value objects. + *

+ *

+ * Supported conversions: + *

+ *
    + *
  • FriendlyId string → {@link UUID}
  • + *
  • FriendlyId string → {@link com.devskiller.friendly_id.type.FriendlyId}
  • + *
+ * + * @see FriendlyIdEncoder + * @since 1.1.1 + */ +public class FriendlyIdDecoder implements Decoder { + + private final Decoder delegate; + + public FriendlyIdDecoder(Decoder delegate) { + this.delegate = delegate; + } + + @Override + public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException { + Object decoded = delegate.decode(response, type); + + if (type == UUID.class && decoded instanceof String stringValue) { + return FriendlyId.toUuid(stringValue); + } + + if (type == com.devskiller.friendly_id.type.FriendlyId.class && decoded instanceof String stringValue) { + return com.devskiller.friendly_id.type.FriendlyId.fromString(stringValue); + } + + return decoded; + } +} diff --git a/friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdEncoder.java b/friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdEncoder.java new file mode 100644 index 0000000..d7c3af0 --- /dev/null +++ b/friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdEncoder.java @@ -0,0 +1,47 @@ +package com.devskiller.friendly_id.openfeign; + +import java.lang.reflect.Type; +import java.util.UUID; + +import com.devskiller.friendly_id.FriendlyId; + +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; + +/** + * Feign encoder that converts UUID and FriendlyId objects to FriendlyId strings in requests. + *

+ * This encoder wraps the default encoder and intercepts UUID and FriendlyId parameters, + * converting them to their FriendlyId string representation before sending the request. + *

+ *

+ * Supported conversions: + *

+ *
    + *
  • {@link UUID} → FriendlyId string
  • + *
  • {@link com.devskiller.friendly_id.type.FriendlyId} → FriendlyId string
  • + *
+ * + * @see FriendlyIdDecoder + * @since 1.1.1 + */ +public class FriendlyIdEncoder implements Encoder { + + private final Encoder delegate; + + public FriendlyIdEncoder(Encoder delegate) { + this.delegate = delegate; + } + + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { + if (object instanceof UUID uuid) { + delegate.encode(FriendlyId.toFriendlyId(uuid), bodyType, template); + } else if (object instanceof com.devskiller.friendly_id.type.FriendlyId friendlyId) { + delegate.encode(friendlyId.toString(), bodyType, template); + } else { + delegate.encode(object, bodyType, template); + } + } +} diff --git a/friendly-id-openfeign/src/test/java/com/devskiller/friendly_id/openfeign/FriendlyIdDecoderTest.java b/friendly-id-openfeign/src/test/java/com/devskiller/friendly_id/openfeign/FriendlyIdDecoderTest.java new file mode 100644 index 0000000..d29565c --- /dev/null +++ b/friendly-id-openfeign/src/test/java/com/devskiller/friendly_id/openfeign/FriendlyIdDecoderTest.java @@ -0,0 +1,78 @@ +package com.devskiller.friendly_id.openfeign; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import feign.Response; +import feign.codec.Decoder; + +import static org.assertj.core.api.Assertions.assertThat; + +class FriendlyIdDecoderTest { + + @Test + void shouldDecodeFriendlyIdStringToUuid() throws IOException { + // given + UUID expectedUuid = UUID.fromString("c3587ec5-0976-497f-8374-61e0c2ea3da5"); + String friendlyIdString = com.devskiller.friendly_id.FriendlyId.toFriendlyId(expectedUuid); + + Decoder delegateDecoder = (response, type) -> friendlyIdString; + FriendlyIdDecoder decoder = new FriendlyIdDecoder(delegateDecoder); + + // when + Object result = decoder.decode(null, UUID.class); + + // then + assertThat(result).isEqualTo(expectedUuid); + } + + @Test + void shouldDecodeFriendlyIdStringToFriendlyIdValueObject() throws IOException { + // given + UUID uuid = UUID.fromString("c3587ec5-0976-497f-8374-61e0c2ea3da5"); + String friendlyIdString = com.devskiller.friendly_id.FriendlyId.toFriendlyId(uuid); + + Decoder delegateDecoder = (response, type) -> friendlyIdString; + FriendlyIdDecoder decoder = new FriendlyIdDecoder(delegateDecoder); + + // when + Object result = decoder.decode(null, com.devskiller.friendly_id.type.FriendlyId.class); + + // then + assertThat(result) + .isInstanceOf(com.devskiller.friendly_id.type.FriendlyId.class) + .extracting(fid -> ((com.devskiller.friendly_id.type.FriendlyId) fid).uuid()) + .isEqualTo(uuid); + } + + @Test + void shouldDelegateOtherTypes() throws IOException { + // given + String regularString = "test"; + Decoder delegateDecoder = (response, type) -> regularString; + FriendlyIdDecoder decoder = new FriendlyIdDecoder(delegateDecoder); + + // when + Object result = decoder.decode(null, String.class); + + // then + assertThat(result).isEqualTo(regularString); + } + + @Test + void shouldDelegateWhenResponseIsNotString() throws IOException { + // given + Integer number = 42; + Decoder delegateDecoder = (response, type) -> number; + FriendlyIdDecoder decoder = new FriendlyIdDecoder(delegateDecoder); + + // when + Object result = decoder.decode(null, UUID.class); + + // then + assertThat(result).isEqualTo(number); + } +} diff --git a/friendly-id-openfeign/src/test/java/com/devskiller/friendly_id/openfeign/FriendlyIdEncoderTest.java b/friendly-id-openfeign/src/test/java/com/devskiller/friendly_id/openfeign/FriendlyIdEncoderTest.java new file mode 100644 index 0000000..ed0ff52 --- /dev/null +++ b/friendly-id-openfeign/src/test/java/com/devskiller/friendly_id/openfeign/FriendlyIdEncoderTest.java @@ -0,0 +1,74 @@ +package com.devskiller.friendly_id.openfeign; + +import java.lang.reflect.Type; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import feign.RequestTemplate; +import feign.codec.Encoder; + +import static org.assertj.core.api.Assertions.assertThat; + +class FriendlyIdEncoderTest { + + @Test + void shouldEncodeUuidAsFriendlyId() { + // given + UUID uuid = UUID.fromString("c3587ec5-0976-497f-8374-61e0c2ea3da5"); + String expectedFriendlyId = com.devskiller.friendly_id.FriendlyId.toFriendlyId(uuid); + + String[] capturedValue = new String[1]; + Encoder delegateEncoder = (object, bodyType, template) -> { + capturedValue[0] = (String) object; + }; + FriendlyIdEncoder encoder = new FriendlyIdEncoder(delegateEncoder); + RequestTemplate template = new RequestTemplate(); + + // when + encoder.encode(uuid, UUID.class, template); + + // then + assertThat(capturedValue[0]).isEqualTo(expectedFriendlyId); + } + + @Test + void shouldEncodeFriendlyIdValueObjectAsString() { + // given + UUID uuid = UUID.fromString("c3587ec5-0976-497f-8374-61e0c2ea3da5"); + com.devskiller.friendly_id.type.FriendlyId friendlyId = com.devskiller.friendly_id.type.FriendlyId.of(uuid); + String expectedString = friendlyId.toString(); + + String[] capturedValue = new String[1]; + Encoder delegateEncoder = (object, bodyType, template) -> { + capturedValue[0] = (String) object; + }; + FriendlyIdEncoder encoder = new FriendlyIdEncoder(delegateEncoder); + RequestTemplate template = new RequestTemplate(); + + // when + encoder.encode(friendlyId, com.devskiller.friendly_id.type.FriendlyId.class, template); + + // then + assertThat(capturedValue[0]).isEqualTo(expectedString); + } + + @Test + void shouldDelegateOtherTypes() { + // given + String regularString = "test"; + + String[] capturedValue = new String[1]; + Encoder delegateEncoder = (object, bodyType, template) -> { + capturedValue[0] = (String) object; + }; + FriendlyIdEncoder encoder = new FriendlyIdEncoder(delegateEncoder); + RequestTemplate template = new RequestTemplate(); + + // when + encoder.encode(regularString, String.class, template); + + // then + assertThat(capturedValue[0]).isEqualTo(regularString); + } +} diff --git a/friendly-id-samples/friendly-id-contracts/pom.xml b/friendly-id-samples/friendly-id-contracts/pom.xml index 8c47473..4e6eccb 100644 --- a/friendly-id-samples/friendly-id-contracts/pom.xml +++ b/friendly-id-samples/friendly-id-contracts/pom.xml @@ -5,20 +5,22 @@ com.devskiller.friendly-id spring-boot-contracts - 1.1.1-SNAPSHOT + ${revision} org.springframework.boot spring-boot-starter-parent - 2.2.2.RELEASE + 4.0.1 + 2.0.0-SNAPSHOT UTF-8 UTF-8 - 1.8 - 2.2.1.RELEASE + 21 + 5.0.1 + 6.0.0 @@ -26,42 +28,37 @@ org.springframework.boot spring-boot-starter-web
- - org.springframework.cloud - spring-cloud-starter-contract-verifier - test - - - com.devskiller.friendly-id - friendly-id-spring-boot-starter - ${project.version} - org.springframework.boot - spring-boot-starter-hateoas + spring-boot-starter-security - org.atteo - evo-inflector - 1.2.2 + org.springframework.security + spring-security-test + test - com.fasterxml.jackson.module - jackson-module-parameter-names + org.springframework.cloud + spring-cloud-starter-contract-verifier + test - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 + io.rest-assured + spring-mock-mvc + ${rest-assured.version} + test - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 + com.devskiller.friendly-id + friendly-id-spring-boot-starter + ${project.version} + org.projectlombok lombok - provided + true @@ -69,6 +66,11 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-webmvc-test + test +
@@ -88,12 +90,26 @@ org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + maven-compiler-plugin - 3.8.0 + 3.14.0 true + + + org.projectlombok + lombok + + @@ -106,7 +122,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.1 org.springframework.cloud @@ -116,6 +131,13 @@ com.devskiller.friendly_id.sample.contracts com.devskiller.friendly_id.sample.contracts.ContractVerifierBase + + + .*authenticated.* + com.devskiller.friendly_id.sample.contracts.AuthenticatedContractBase + + + JUNIT5 diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/AdminController.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/AdminController.java new file mode 100644 index 0000000..d23a0d9 --- /dev/null +++ b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/AdminController.java @@ -0,0 +1,15 @@ +package com.devskiller.friendly_id.sample.contracts; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin") +public class AdminController { + + @GetMapping("/status") + public String status() { + return "OK"; + } +} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarController.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarController.java deleted file mode 100644 index 787711f..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarController.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import com.devskiller.friendly_id.sample.contracts.domain.Bar; -import com.devskiller.friendly_id.sample.contracts.domain.Foo; -import org.springframework.hateoas.server.ExposesResourceFor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.UUID; - -@RestController -@ExposesResourceFor(BarResource.class) -@RequestMapping("/foos/{fooId}/bars") -public class BarController { - - private final BarResourceAssembler assembler; - - public BarController(BarResourceAssembler assembler) { - this.assembler = assembler; - } - - @GetMapping("/{id}") - public BarResource getBar(@PathVariable UUID fooId, @PathVariable UUID id) { - return assembler.toModel(new Bar(id, "Bar", new Foo(fooId, "Root Foo"))); - } - -} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarResource.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarResource.java deleted file mode 100644 index a7032c2..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarResource.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import lombok.Value; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.server.core.Relation; - -@Relation(value = "bar", collectionRelation = "bars") -@Value -class BarResource extends RepresentationModel { - - private String name; - -} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarResourceAssembler.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarResourceAssembler.java deleted file mode 100644 index 8f672a7..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarResourceAssembler.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import com.devskiller.friendly_id.FriendlyId; -import com.devskiller.friendly_id.sample.contracts.domain.Bar; -import com.devskiller.friendly_id.sample.contracts.domain.Foo; -import org.springframework.hateoas.server.LinkRelationProvider; -import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; -import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderFactory; - -public class BarResourceAssembler extends RepresentationModelAssemblerSupport { - - LinkRelationProvider relProvider; - - public BarResourceAssembler() { - super(BarController.class, BarResource.class); - } - - public BarResourceAssembler(LinkRelationProvider relProvider) { - super(BarController.class, BarResource.class); - this.relProvider = relProvider; - } - - @Override - public BarResource toModel(Bar entity) { - BarResource resource = new BarResource(entity.getName()); - WebMvcLinkBuilderFactory factory = new WebMvcLinkBuilderFactory(); - resource.add(factory.linkTo(FooController.class, FriendlyId.toFriendlyId(entity.getFoo().getId())) - .withRel(relProvider.getCollectionResourceRelFor(Foo.class))); - resource.add(factory.linkTo(BarController.class, FriendlyId.toFriendlyId(entity.getFoo().getId())).slash(FriendlyId.toFriendlyId(entity.getId())).withSelfRel()); - return resource; - } - -} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooController.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooController.java deleted file mode 100644 index c8065cc..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooController.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import com.devskiller.friendly_id.FriendlyId; -import com.devskiller.friendly_id.sample.contracts.domain.Foo; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.hateoas.server.EntityLinks; -import org.springframework.hateoas.server.ExposesResourceFor; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.lang.invoke.MethodHandles; -import java.util.UUID; - - -@RestController -@ExposesResourceFor(FooResource.class) -@RequestMapping("/foos") -public class FooController { - - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final EntityLinks entityLinks; - private final FooResourceAssembler assembler; - - public FooController(EntityLinks entityLinks) { - this.entityLinks = entityLinks; - this.assembler = new FooResourceAssembler(); - } - - @GetMapping("/{id}") - public FooResource get(@PathVariable UUID id) { - log.info("Get {}", id); - Foo foo = new Foo(id, "Foo"); - return assembler.toModel(foo); - } - - @PutMapping("/{id}") - public HttpEntity update(@PathVariable UUID id, @RequestBody FooResource fooResource) { - log.info("Update {} : {}", id, fooResource); - Foo entity = new Foo(fooResource.getUuid(), fooResource.getName()); - return ResponseEntity.ok(assembler.toModel(entity)); - } - - @PostMapping - public HttpEntity create(@RequestBody FooResource fooResource) { - HttpHeaders headers = new HttpHeaders(); - Foo entity = new Foo(fooResource.getUuid(), "Foo"); - - // ... - - headers.setLocation(entityLinks.linkToItemResource(FooResource.class, FriendlyId.toFriendlyId(entity.getId())).toUri()); - return new ResponseEntity<>(headers, HttpStatus.CREATED); - } - -} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooResource.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooResource.java deleted file mode 100644 index 6a33beb..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooResource.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import lombok.Value; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.server.core.Relation; - -import java.util.UUID; - -@Relation(value = "foos") -@Value -public class FooResource extends RepresentationModel { - - private final UUID uuid; - private final String name; - -} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooResourceAssembler.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooResourceAssembler.java deleted file mode 100644 index 3b7e367..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooResourceAssembler.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import com.devskiller.friendly_id.FriendlyId; -import com.devskiller.friendly_id.sample.contracts.domain.Foo; -import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; -import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderFactory; - -public class FooResourceAssembler extends RepresentationModelAssemblerSupport { - - public FooResourceAssembler() { - super(FooController.class, FooResource.class); - } - - @Override - public FooResource toModel(Foo entity) { - WebMvcLinkBuilderFactory factory = new WebMvcLinkBuilderFactory(); - FooResource resource = new FooResource(entity.getId(), entity.getName()); - resource.add(factory.linkTo(FooController.class).slash(FriendlyId.toFriendlyId(entity.getId())).withSelfRel()); - return resource; - } -} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/Item.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/Item.java new file mode 100644 index 0000000..b9e7802 --- /dev/null +++ b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/Item.java @@ -0,0 +1,23 @@ +package com.devskiller.friendly_id.sample.contracts; + +import java.util.UUID; + +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * Example record demonstrating different UUID serialization formats. + * + * @param id UUID serialized as FriendlyId string (default behavior) + * @param rawId UUID serialized as raw UUID string + * @param friendlyUuid UUID explicitly serialized as FriendlyId string + * @param friendlyId FriendlyId value object type + */ +public record Item( + UUID id, + @IdFormat(FriendlyIdFormat.RAW) UUID rawId, + @IdFormat(FriendlyIdFormat.URL62) UUID friendlyUuid, + FriendlyId friendlyId +) { +} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/ItemController.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/ItemController.java new file mode 100644 index 0000000..1e2d4bb --- /dev/null +++ b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/ItemController.java @@ -0,0 +1,33 @@ +package com.devskiller.friendly_id.sample.contracts; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.devskiller.friendly_id.type.FriendlyId; + +@Slf4j +@RestController +@RequestMapping("/items") +public class ItemController { + + @GetMapping("/{id}") + public Item get(@PathVariable UUID id) { + log.info("Get {}", id); + return new Item(id, id, id, FriendlyId.of(id)); + } + + @PostMapping + public Item create(@RequestBody Item item) { + log.info("Create {}", item); + var uuid = item.id() != null ? item.id() : UUID.randomUUID(); + return new Item(uuid, uuid, uuid, FriendlyId.of(uuid)); + } +} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/JsonConfiguration.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/JsonConfiguration.java deleted file mode 100644 index bab28e6..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/JsonConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.hateoas.server.LinkRelationProvider; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class JsonConfiguration implements WebMvcConfigurer { - - // This is declared as part of WebMVC slice, used in testing - @Bean - public FooResourceAssembler fooResourceAssembler() { - return new FooResourceAssembler(); - } - - // This is declared as part of WebMVC slice, used in testing - @Bean - public BarResourceAssembler barResourceAssembler(LinkRelationProvider relProvider) { - return new BarResourceAssembler(relProvider); - } -} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/SecurityConfig.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/SecurityConfig.java new file mode 100644 index 0000000..e46f026 --- /dev/null +++ b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/SecurityConfig.java @@ -0,0 +1,25 @@ +package com.devskiller.friendly_id.sample.contracts; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .authorizeHttpRequests(auth -> auth + .requestMatchers("/admin/**").authenticated() + .requestMatchers(org.springframework.http.HttpMethod.POST, "/items").authenticated() + .anyRequest().permitAll() + ) + .csrf(csrf -> csrf.disable()) + .httpBasic(basic -> {}) + .build(); + } +} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/domain/Bar.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/domain/Bar.java deleted file mode 100644 index 3844548..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/domain/Bar.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts.domain; - -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.util.UUID; - -@Data -@AllArgsConstructor -public class Bar { - - private UUID id; - private String name; - - private Foo foo; - -} diff --git a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/domain/Foo.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/domain/Foo.java deleted file mode 100644 index 343fe77..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/domain/Foo.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts.domain; - -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.util.UUID; - -@Data -@AllArgsConstructor -public class Foo { - - private UUID id; - private String name; - -} diff --git a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/AuthenticatedContractBase.java b/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/AuthenticatedContractBase.java new file mode 100644 index 0000000..0e2066b --- /dev/null +++ b/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/AuthenticatedContractBase.java @@ -0,0 +1,30 @@ +package com.devskiller.friendly_id.sample.contracts; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import org.junit.jupiter.api.BeforeEach; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.WebApplicationContext; + +import com.devskiller.friendly_id.spring.EnableFriendlyId; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + +@WebMvcTest +@EnableFriendlyId +@WithMockUser(username = "admin", roles = "ADMIN") +@Import(SecurityConfig.class) +public abstract class AuthenticatedContractBase { + + @Autowired + private WebApplicationContext context; + + @BeforeEach + public void setUp() { + RestAssuredMockMvc.webAppContextSetup(context, springSecurity()); + } + +} diff --git a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/BarControllerTest.java b/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/BarControllerTest.java deleted file mode 100644 index 3000a00..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/BarControllerTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; - -import com.devskiller.friendly_id.spring.EnableFriendlyId; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@RunWith(SpringRunner.class) -@WebMvcTest(BarController.class) -@EnableFriendlyId -public class BarControllerTest { - - @Autowired - MockMvc mockMvc; - - @Test - public void shouldGet() throws Exception { - mockMvc.perform(get("/foos/{fooId}/bars/{barId}", "foo", "bar")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/hal+json")) - .andExpect(jsonPath("$.name", is("Bar"))) - .andExpect(jsonPath("$._links.self.href", is("http://localhost/foos/foo/bars/bar"))) - .andExpect(jsonPath("$._links.foos.href", is("http://localhost/foos"))); - } - -} diff --git a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/ContractVerifierBase.java b/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/ContractVerifierBase.java index 8c2cb4b..ff207b9 100644 --- a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/ContractVerifierBase.java +++ b/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/ContractVerifierBase.java @@ -1,27 +1,28 @@ package com.devskiller.friendly_id.sample.contracts; import io.restassured.module.mockmvc.RestAssuredMockMvc; -import org.junit.Before; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.web.context.WebApplicationContext; import com.devskiller.friendly_id.spring.EnableFriendlyId; -@RunWith(SpringRunner.class) +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + @WebMvcTest @EnableFriendlyId +@Import(SecurityConfig.class) public abstract class ContractVerifierBase { @Autowired private WebApplicationContext context; - @Before + @BeforeEach public void setUp() { - RestAssuredMockMvc.webAppContextSetup(context); + RestAssuredMockMvc.webAppContextSetup(context, springSecurity()); } } diff --git a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/FooControllerTest.java b/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/FooControllerTest.java deleted file mode 100644 index fae95d7..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/FooControllerTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; - -import com.devskiller.friendly_id.spring.EnableFriendlyId; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@RunWith(SpringRunner.class) -@WebMvcTest(FooController.class) -@EnableFriendlyId -public class FooControllerTest { - - @Autowired - MockMvc mockMvc; - - @Test - public void shouldGet() throws Exception { - mockMvc.perform(get("/foos/{id}", "cafe")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/hal+json")) - .andExpect(jsonPath("$.uuid", is("cafe"))) - .andExpect(jsonPath("$._links.self.href", is("http://localhost/foos/cafe"))); - } - - @Test - public void shouldCreate() throws Exception { - mockMvc.perform(post("/foos/") - .content("{\"uuid\":\"newFoo\",\"name\":\"Very New Foo\"}") - .contentType("application/hal+json")) - .andDo(print()) - .andExpect(header().string("Location", "http://localhost/foos/newFoo")) - .andExpect(status().isCreated()); - } - - @Test - public void update() throws Exception { - mockMvc.perform(put("/foos/{id}", "foo") - .content("{\"uuid\":\"foo\",\"name\":\"Sample Foo\"}") - .contentType("application/hal+json;charset=UTF-8")) - .andDo(print()) - .andExpect(status().isOk()); - } - -} diff --git a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/MvcTest.java b/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/MvcTest.java deleted file mode 100644 index 2c5f697..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/MvcTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import com.devskiller.friendly_id.FriendlyId; -import com.devskiller.friendly_id.jackson.FriendlyIdModule; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; -import io.restassured.module.mockmvc.RestAssuredMockMvc; -import org.junit.Before; -import org.springframework.core.convert.converter.Converter; -import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.hateoas.server.EntityLinks; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; - -import java.util.UUID; - -import static org.mockito.Mockito.mock; -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; - -public class MvcTest { - - protected StandaloneMockMvcBuilder mockMvcBuilder; - - @Before - public void setup() { - mockMvcBuilder = standaloneSetup(new FooController(mock(EntityLinks.class))); - DefaultFormattingConversionService service = new DefaultFormattingConversionService(); - service.addConverter(new StringToUuidConverter()); - mockMvcBuilder.setMessageConverters(jackson2HttpMessageConverter()).setConversionService(service); - RestAssuredMockMvc.standaloneSetup(mockMvcBuilder); - } - - public static class StringToUuidConverter implements Converter { - - @Override - public UUID convert(String id) { - return FriendlyId.toUuid(id); - } - } - - private MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() { - MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); - Jackson2ObjectMapperBuilder builder = this.jacksonBuilder(); - converter.setObjectMapper(builder.build()); - return converter; - } - - protected Jackson2ObjectMapperBuilder jacksonBuilder() { - Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); - builder.modules(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES), new JavaTimeModule(), new FriendlyIdModule()); - builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - builder.simpleDateFormat("yyyy-MM-dd"); - builder.indentOutput(true); - return builder; - } - - -} diff --git a/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/AdminUnauthorized.groovy b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/AdminUnauthorized.groovy new file mode 100644 index 0000000..23e81bf --- /dev/null +++ b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/AdminUnauthorized.groovy @@ -0,0 +1,16 @@ +package contracts + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + description "should return 401 when accessing admin endpoint without authentication" + + request { + method 'GET' + url '/admin/status' + } + + response { + status 401 + } +} diff --git a/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/CreateItemUnauthorized.groovy b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/CreateItemUnauthorized.groovy new file mode 100644 index 0000000..a0787b9 --- /dev/null +++ b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/CreateItemUnauthorized.groovy @@ -0,0 +1,22 @@ +package contracts + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + description "should return 401 when creating item without authentication" + + request { + method 'POST' + url '/items' + headers { + contentType applicationJson() + } + body( + id: "unauthorizedItem" + ) + } + + response { + status 401 + } +} diff --git a/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetFoo.groovy b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetFoo.groovy deleted file mode 100644 index 981de22..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetFoo.groovy +++ /dev/null @@ -1,25 +0,0 @@ -org.springframework.cloud.contract.spec.Contract.make { - request { - method 'GET' - url '/foos/caffe' - headers { - applicationJsonUtf8() - } - } - response { - status 200 - body( - uuid: 'caffe', - name: 'Foo', - _links: [ - self: [ - href: 'http://localhost/foos/caffe' - ] - - ] - ) - headers { - applicationJsonUtf8() - } - } -} \ No newline at end of file diff --git a/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetItem.groovy b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetItem.groovy new file mode 100644 index 0000000..304e016 --- /dev/null +++ b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetItem.groovy @@ -0,0 +1,25 @@ +package contracts + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + description "should return item with all ID formats" + + request { + method 'GET' + url '/items/testItemId' + } + + response { + status 200 + headers { + contentType applicationJson() + } + body( + id: "testItemId", + rawId: $(regex('[a-f0-9-]{36}')), + friendlyUuid: "testItemId", + friendlyId: "testItemId" + ) + } +} diff --git a/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/authenticated/AdminAuthorized.groovy b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/authenticated/AdminAuthorized.groovy new file mode 100644 index 0000000..6150417 --- /dev/null +++ b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/authenticated/AdminAuthorized.groovy @@ -0,0 +1,17 @@ +package contracts.authenticated + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + description "should return OK when accessing admin endpoint with authentication" + + request { + method 'GET' + url '/admin/status' + } + + response { + status 200 + body "OK" + } +} diff --git a/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/authenticated/CreateItem.groovy b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/authenticated/CreateItem.groovy new file mode 100644 index 0000000..9b96510 --- /dev/null +++ b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/authenticated/CreateItem.groovy @@ -0,0 +1,31 @@ +package contracts.authenticated + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + description "should create item when authenticated" + + request { + method 'POST' + url '/items' + headers { + contentType applicationJson() + } + body( + id: "authItemId" + ) + } + + response { + status 200 + headers { + contentType applicationJson() + } + body( + id: "authItemId", + rawId: $(regex('[a-f0-9-]{36}')), + friendlyUuid: "authItemId", + friendlyId: "authItemId" + ) + } +} diff --git a/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml b/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml index 7858da6..3fd99ae 100644 --- a/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml @@ -5,19 +5,20 @@ com.devskiller.friendly-id spring-boot-customized - 1.1.1-SNAPSHOT + ${revision} org.springframework.boot spring-boot-starter-parent - 2.2.2.RELEASE + 4.0.1 + 2.0.0-SNAPSHOT UTF-8 UTF-8 - 1.8 + 21 @@ -31,15 +32,12 @@ ${project.version} - - com.fasterxml.jackson.module - jackson-module-parameter-names - + org.projectlombok lombok - provided + true @@ -47,32 +45,50 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-webmvc-test + test + - org.springframework.boot - spring-boot-maven-plugin - - + org.apache.maven.plugins maven-compiler-plugin - 3.8.1 true + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + org.apache.maven.plugins - maven-deploy-plugin + maven-surefire-plugin - true + -XX:+EnableDynamicAgentLoading - org.jacoco - jacoco-maven-plugin - 0.8.5 + org.apache.maven.plugins + maven-deploy-plugin true diff --git a/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/Bar.java b/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/Bar.java deleted file mode 100644 index 2de1b20..0000000 --- a/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/Bar.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.devskiller.friendly_id.sample.customized; - -import java.util.UUID; - -import lombok.Value; - -import com.devskiller.friendly_id.jackson.IdFormat; - -import static com.devskiller.friendly_id.jackson.FriendlyIdFormat.RAW; - -@Value -class Bar { - - private final UUID friendlyId; - - @IdFormat(RAW) - private final UUID uuid; - - public Bar(UUID friendlyId, @IdFormat(RAW) UUID uuid) { - this.friendlyId = friendlyId; - this.uuid = uuid; - } -} diff --git a/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/BarController.java b/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/BarController.java deleted file mode 100644 index d0dfd90..0000000 --- a/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/BarController.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.devskiller.friendly_id.sample.customized; - -import java.lang.invoke.MethodHandles; -import java.util.UUID; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/bars") -public class BarController { - - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final FooService fooService; - - public BarController(FooService fooService) { - this.fooService = fooService; - } - - @GetMapping("/{id}") - public Bar get(@PathVariable UUID id) { - log.info("get {}", id); - return fooService.find(id); - } - - @PutMapping("/{id}") - public void getBar(@PathVariable UUID id, @RequestBody Bar body) { - fooService.update(id, body); - } -} diff --git a/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/FooService.java b/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/FooService.java deleted file mode 100644 index 521afd3..0000000 --- a/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/FooService.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.devskiller.friendly_id.sample.customized; - -import java.util.UUID; - -import org.springframework.stereotype.Service; - -@Service -class FooService { - - Bar find(UUID uuid) { - System.out.println("find: " + uuid); - return new Bar(uuid, uuid); - } - - void update(UUID id, Bar bar) { - System.out.println("update: " + id + ":" + bar); - } -} diff --git a/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/Item.java b/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/Item.java new file mode 100644 index 0000000..7521ee7 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/Item.java @@ -0,0 +1,23 @@ +package com.devskiller.friendly_id.sample.customized; + +import java.util.UUID; + +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * Example record demonstrating different UUID serialization formats. + * + * @param id UUID serialized as FriendlyId string (default behavior) + * @param rawId UUID serialized as raw UUID string + * @param friendlyUuid UUID explicitly serialized as FriendlyId string + * @param friendlyId FriendlyId value object type + */ +public record Item( + UUID id, + @IdFormat(FriendlyIdFormat.RAW) UUID rawId, + @IdFormat(FriendlyIdFormat.URL62) UUID friendlyUuid, + FriendlyId friendlyId +) { +} diff --git a/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/ItemController.java b/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/ItemController.java new file mode 100644 index 0000000..9479ef6 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/ItemController.java @@ -0,0 +1,42 @@ +package com.devskiller.friendly_id.sample.customized; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping("/items") +public class ItemController { + + private final ItemService itemService; + + public ItemController(ItemService itemService) { + this.itemService = itemService; + } + + @GetMapping("/{id}") + public Item get(@PathVariable UUID id) { + log.info("get {}", id); + return itemService.find(id); + } + + @PostMapping + public Item create(@RequestBody Item item) { + log.info("create {}", item); + return itemService.create(item); + } + + @PutMapping("/{id}") + public void update(@PathVariable UUID id, @RequestBody Item body) { + itemService.update(id, body); + } +} diff --git a/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/ItemService.java b/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/ItemService.java new file mode 100644 index 0000000..732546e --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/ItemService.java @@ -0,0 +1,31 @@ +package com.devskiller.friendly_id.sample.customized; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; + +import com.devskiller.friendly_id.type.FriendlyId; + +@Slf4j +@Service +public class ItemService { + + public Item find(UUID uuid) { + log.info("find: {}", uuid); + return new Item(uuid, uuid, uuid, FriendlyId.of(uuid)); + } + + public Item create(Item item) { + if (item.id() == null) { + var uuid = UUID.randomUUID(); + return new Item(uuid, uuid, uuid, FriendlyId.of(uuid)); + } + return item; + } + + public void update(UUID id, Item item) { + log.info("update: {}:{}", id, item); + } +} diff --git a/friendly-id-samples/friendly-id-spring-boot-customized/src/test/java/com/devskiller/friendly_id/sample/customized/ApplicationTest.java b/friendly-id-samples/friendly-id-spring-boot-customized/src/test/java/com/devskiller/friendly_id/sample/customized/ApplicationTest.java index 59bfa36..a38394a 100644 --- a/friendly-id-samples/friendly-id-spring-boot-customized/src/test/java/com/devskiller/friendly_id/sample/customized/ApplicationTest.java +++ b/friendly-id-samples/friendly-id-spring-boot-customized/src/test/java/com/devskiller/friendly_id/sample/customized/ApplicationTest.java @@ -1,88 +1,118 @@ package com.devskiller.friendly_id.sample.customized; -import com.devskiller.friendly_id.FriendlyId; -import com.devskiller.friendly_id.spring.EnableFriendlyId; -import org.junit.Test; -import org.junit.runner.RunWith; +import java.util.UUID; + +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import java.util.UUID; +import com.devskiller.friendly_id.FriendlyId; +import com.devskiller.friendly_id.spring.EnableFriendlyId; +import static com.devskiller.friendly_id.FriendlyId.toFriendlyId; import static com.devskiller.friendly_id.FriendlyId.toUuid; import static org.hamcrest.CoreMatchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -@RunWith(SpringRunner.class) -@WebMvcTest(BarController.class) +@WebMvcTest(ItemController.class) @EnableFriendlyId -public class ApplicationTest { +class ApplicationTest { @Autowired MockMvc mockMvc; - @MockBean - FooService fooService; + @MockitoBean + ItemService itemService; @Test - public void shouldSerialize() throws Exception { - + void shouldSerializeAllIdFormats() throws Exception { // given UUID uuid = UUID.randomUUID(); - given(fooService.find(uuid)).willReturn(new Bar(uuid, uuid)); + String friendlyId = toFriendlyId(uuid); + var item = new Item(uuid, uuid, uuid, com.devskiller.friendly_id.type.FriendlyId.of(uuid)); + given(itemService.find(uuid)).willReturn(item); // expect - mockMvc.perform(get("/bars/{id}", FriendlyId.toFriendlyId(uuid))) + mockMvc.perform(get("/items/{id}", friendlyId) + .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.friendlyId", is(FriendlyId.toFriendlyId(uuid)))) - .andExpect(jsonPath("$.uuid", is(uuid.toString()))); + .andExpect(jsonPath("$.id", is(friendlyId))) + .andExpect(jsonPath("$.rawId", is(uuid.toString()))) + .andExpect(jsonPath("$.friendlyUuid", is(friendlyId))) + .andExpect(jsonPath("$.friendlyId", is(friendlyId))); } @Test - public void shouldDeserialize() throws Exception { - + void shouldDeserializeAndCreate() throws Exception { // given UUID uuid = UUID.randomUUID(); - String json = "{\"friendlyId\":\"" + FriendlyId.toFriendlyId(uuid) + "\",\"uuid\":\"" + uuid + "\"}"; + String friendlyId = toFriendlyId(uuid); + String json = """ + {"id": "%s", "rawId": "%s", "friendlyUuid": "%s", "friendlyId": "%s"} + """.formatted(friendlyId, uuid, friendlyId, friendlyId); + + var item = new Item(uuid, uuid, uuid, com.devskiller.friendly_id.type.FriendlyId.of(uuid)); + given(itemService.create(any(Item.class))).willReturn(item); // when - mockMvc.perform(put("/bars/{id}", FriendlyId.toFriendlyId(uuid)) - .content(json) - .contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform(post("/items") + .content(json) + .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()); // then - then(fooService) - .should().update(uuid, new Bar(uuid, uuid)); + then(itemService).should().create(any(Item.class)); } @Test - public void sampleTestUsingPseudoUuid() throws Exception { + void shouldDeserializeAndUpdate() throws Exception { + // given + UUID uuid = UUID.randomUUID(); + String friendlyId = toFriendlyId(uuid); + String json = """ + {"id": "%s"} + """.formatted(friendlyId); + + // when + mockMvc.perform(put("/items/{id}", friendlyId) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()); + // then + then(itemService).should().update(eq(uuid), any(Item.class)); + } + + @Test + void shouldWorkWithPseudoUuid() throws Exception { // given - UUID barId = toUuid("barId"); - given(fooService.find(barId)).willReturn(new Bar(barId, barId)); + UUID itemId = toUuid("itemId"); + String friendlyId = toFriendlyId(itemId); + var item = new Item(itemId, itemId, itemId, com.devskiller.friendly_id.type.FriendlyId.of(itemId)); + given(itemService.find(itemId)).willReturn(item); // expect - mockMvc.perform(get("/bars/{id}", "barId")) + mockMvc.perform(get("/items/{id}", "itemId") + .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.friendlyId", is("barId"))) - .andExpect(jsonPath("$.uuid", is(barId.toString()))); - - System.out.println(barId); + .andExpect(jsonPath("$.id", is(friendlyId))) + .andExpect(jsonPath("$.rawId", is(itemId.toString()))); } } diff --git a/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml b/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml index 7da749c..14bd415 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml @@ -5,19 +5,20 @@ com.devskiller.friendly-id spring-boot-hateos - 1.1.1-SNAPSHOT + ${revision} org.springframework.boot spring-boot-starter-parent - 2.2.2.RELEASE + 4.0.1 + 2.0.0-SNAPSHOT UTF-8 UTF-8 - 1.8 + 21 @@ -37,25 +38,14 @@ org.atteo evo-inflector - 1.2.2 - - - com.fasterxml.jackson.module - jackson-module-parameter-names - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 + 1.3 + org.projectlombok lombok - provided + true @@ -63,17 +53,39 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-webmvc-test + test + - org.springframework.boot - spring-boot-maven-plugin + org.apache.maven.plugins + maven-compiler-plugin + + true + + + org.projectlombok + lombok + + + - maven-compiler-plugin - 3.8.0 + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + org.apache.maven.plugins diff --git a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/BarResource.java b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/BarResource.java index ab79b75..7e58445 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/BarResource.java +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/BarResource.java @@ -1,13 +1,19 @@ package com.devskiller.friendly_id.sample.hateos; -import lombok.Value; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.server.core.Relation; @Relation(value = "bar", collectionRelation = "bars") -@Value +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor class BarResource extends RepresentationModel { - private String name; + String name; } diff --git a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/BarResourceAssembler.java b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/BarResourceAssembler.java index 1055627..e7199aa 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/BarResourceAssembler.java +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/BarResourceAssembler.java @@ -2,10 +2,12 @@ import com.devskiller.friendly_id.sample.hateos.domain.Bar; import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; -import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderFactory; +import org.springframework.stereotype.Component; -import static com.devskiller.friendly_id.FriendlyId.toFriendlyId; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; +@Component public class BarResourceAssembler extends RepresentationModelAssemblerSupport { public BarResourceAssembler() { @@ -14,10 +16,14 @@ public BarResourceAssembler() { @Override public BarResource toModel(Bar entity) { - BarResource resource = new BarResource(entity.getName()); - WebMvcLinkBuilderFactory factory = new WebMvcLinkBuilderFactory(); - resource.add(factory.linkTo(FooController.class).withRel("foos")); - resource.add(factory.linkTo(BarController.class, toFriendlyId(entity.getFoo().getId())).slash(toFriendlyId(entity.getId())).withSelfRel()); + BarResource resource = new BarResource(entity.name()); + + // Modern Spring HATEOAS 2.x - methodOn() triggers automatic FriendlyId conversion + resource.add(linkTo(FooController.class).withRel("foos")); + resource.add(linkTo(methodOn(BarController.class) + .getBar(entity.foo().id(), entity.id())) + .withSelfRel()); + return resource; } diff --git a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooController.java b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooController.java index 896b900..27ac6fc 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooController.java +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooController.java @@ -1,29 +1,24 @@ package com.devskiller.friendly_id.sample.hateos; import com.devskiller.friendly_id.sample.hateos.domain.Foo; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.hateoas.server.ExposesResourceFor; import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; -import java.lang.invoke.MethodHandles; import java.net.URI; import java.util.UUID; -import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.on; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; +@Slf4j @RestController @ExposesResourceFor(FooResource.class) @RequestMapping("/foos") public class FooController { - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final FooResourceAssembler assembler; public FooController(FooResourceAssembler assembler) { @@ -48,18 +43,14 @@ public HttpEntity update(@PathVariable UUID id, @RequestBody FooRes @PostMapping public HttpEntity create(@RequestBody FooResource fooResource) { - HttpHeaders headers = new HttpHeaders(); - Foo entity = new Foo(fooResource.getUuid(), "Foo"); + log.info("Create {}", fooResource.getUuid()); - // ... - URI location = MvcUriComponentsBuilder.fromMethodCall(on(getClass()) + // Modern Spring HATEOAS 2.x - methodOn() triggers Spring's FriendlyId conversion + URI location = linkTo(methodOn(FooController.class) .get(fooResource.getUuid())) - .buildAndExpand() .toUri(); - headers.setLocation(location); - - return new ResponseEntity<>(headers, HttpStatus.CREATED); + return ResponseEntity.created(location).build(); } } diff --git a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooResource.java b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooResource.java index 9c79633..1b10bbb 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooResource.java +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooResource.java @@ -1,15 +1,16 @@ package com.devskiller.friendly_id.sample.hateos; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonUnwrapped; -import lombok.Value; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.server.core.Relation; +import java.util.List; import java.util.UUID; @Relation(value = "foos") -@Value public class FooResource extends RepresentationModel { private final UUID uuid; @@ -17,4 +18,29 @@ public class FooResource extends RepresentationModel { @JsonUnwrapped private final CollectionModel embeddeds; + // Full constructor + public FooResource(UUID uuid, String name, CollectionModel embeddeds) { + this.uuid = uuid; + this.name = name; + this.embeddeds = embeddeds; + } + + // Constructor for creating resources without embedded collections (for deserialization) + @JsonCreator + public FooResource(@JsonProperty("uuid") UUID uuid, @JsonProperty("name") String name) { + this(uuid, name, CollectionModel.of(List.of())); + } + + public UUID getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public CollectionModel getEmbeddeds() { + return embeddeds; + } + } diff --git a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooResourceAssembler.java b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooResourceAssembler.java index 1e60b7a..0e18407 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooResourceAssembler.java +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooResourceAssembler.java @@ -4,30 +4,39 @@ import com.devskiller.friendly_id.sample.hateos.domain.Foo; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; -import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderFactory; +import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; import java.util.UUID; -import static com.devskiller.friendly_id.FriendlyId.toFriendlyId; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; +@Component public class FooResourceAssembler extends RepresentationModelAssemblerSupport { - public FooResourceAssembler() { + private final BarResourceAssembler barResourceAssembler; + + public FooResourceAssembler(BarResourceAssembler barResourceAssembler) { super(FooController.class, FooResource.class); + this.barResourceAssembler = barResourceAssembler; } @Override public FooResource toModel(Foo entity) { - BarResourceAssembler barResourceAssembler = new BarResourceAssembler(); - List bars = Arrays.asList(new Bar(UUID.randomUUID(), "bar one", entity), - new Bar(UUID.randomUUID(), "bar two", entity)); + List bars = Arrays.asList( + new Bar(UUID.randomUUID(), "bar one", entity), + new Bar(UUID.randomUUID(), "bar two", entity) + ); CollectionModel barResources = barResourceAssembler.toCollectionModel(bars); - WebMvcLinkBuilderFactory factory = new WebMvcLinkBuilderFactory(); - FooResource resource = new FooResource(entity.getId(), entity.getName(), barResources); + FooResource resource = new FooResource(entity.id(), entity.name(), barResources); + + // Modern Spring HATEOAS 2.x - methodOn() triggers automatic FriendlyId conversion + resource.add(linkTo(methodOn(FooController.class) + .get(entity.id())) + .withSelfRel()); - resource.add(factory.linkTo(FooController.class).slash(toFriendlyId(entity.getId())).withSelfRel()); return resource; } } diff --git a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/JsonConfiguration.java b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/JsonConfiguration.java deleted file mode 100644 index 9124469..0000000 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/JsonConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.devskiller.friendly_id.sample.hateos; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class JsonConfiguration implements WebMvcConfigurer { - - // This is declared as part of WebMVC slice, used in testing - @Bean - public FooResourceAssembler fooResourceAssembler() { - return new FooResourceAssembler(); - } - - // This is declared as part of WebMVC slice, used in testing - @Bean - public BarResourceAssembler barResourceAssembler() { - return new BarResourceAssembler(); - } - -} diff --git a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/domain/Bar.java b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/domain/Bar.java index 7bd5961..278f6bd 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/domain/Bar.java +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/domain/Bar.java @@ -1,17 +1,6 @@ package com.devskiller.friendly_id.sample.hateos.domain; -import lombok.AllArgsConstructor; -import lombok.Data; - import java.util.UUID; -@Data -@AllArgsConstructor -public class Bar { - - private UUID id; - private String name; - - private Foo foo; - +public record Bar(UUID id, String name, Foo foo) { } diff --git a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/domain/Foo.java b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/domain/Foo.java index 3c5ae28..87356b0 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/domain/Foo.java +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/domain/Foo.java @@ -1,15 +1,6 @@ package com.devskiller.friendly_id.sample.hateos.domain; -import lombok.AllArgsConstructor; -import lombok.Data; - import java.util.UUID; -@Data -@AllArgsConstructor -public class Foo { - - private UUID id; - private String name; - +public record Foo(UUID id, String name) { } diff --git a/friendly-id-samples/friendly-id-spring-boot-hateos/src/test/java/com/devskiller/friendly_id/sample/hateos/BarControllerTest.java b/friendly-id-samples/friendly-id-spring-boot-hateos/src/test/java/com/devskiller/friendly_id/sample/hateos/BarControllerTest.java index 8721615..3fe3717 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/src/test/java/com/devskiller/friendly_id/sample/hateos/BarControllerTest.java +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/src/test/java/com/devskiller/friendly_id/sample/hateos/BarControllerTest.java @@ -1,15 +1,12 @@ package com.devskiller.friendly_id.sample.hateos; -import org.junit.Test; -import org.junit.runner.RunWith; - +import com.devskiller.friendly_id.spring.EnableFriendlyId; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; -import com.devskiller.friendly_id.spring.EnableFriendlyId; - import static org.hamcrest.CoreMatchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @@ -17,16 +14,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(SpringRunner.class) @WebMvcTest(BarController.class) @EnableFriendlyId -public class BarControllerTest { +@Import({FooResourceAssembler.class, BarResourceAssembler.class}) +class BarControllerTest { @Autowired MockMvc mockMvc; @Test - public void shouldGet() throws Exception { + void shouldGet() throws Exception { mockMvc.perform(get("/foos/{fooId}/bars/{barId}", "foo", "bar")) .andDo(print()) .andExpect(status().isOk()) diff --git a/friendly-id-samples/friendly-id-spring-boot-hateos/src/test/java/com/devskiller/friendly_id/sample/hateos/FooControllerTest.java b/friendly-id-samples/friendly-id-spring-boot-hateos/src/test/java/com/devskiller/friendly_id/sample/hateos/FooControllerTest.java index f86b842..42866a3 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/src/test/java/com/devskiller/friendly_id/sample/hateos/FooControllerTest.java +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/src/test/java/com/devskiller/friendly_id/sample/hateos/FooControllerTest.java @@ -1,15 +1,12 @@ package com.devskiller.friendly_id.sample.hateos; -import org.junit.Test; -import org.junit.runner.RunWith; - +import com.devskiller.friendly_id.spring.EnableFriendlyId; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; -import com.devskiller.friendly_id.spring.EnableFriendlyId; - import static org.hamcrest.CoreMatchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -20,16 +17,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(SpringRunner.class) @WebMvcTest -@EnableFriendlyId // STRANGE: Why this is required? -public class FooControllerTest { +@EnableFriendlyId // Required for UUID <-> FriendlyID conversion in path variables +@Import({FooResourceAssembler.class, BarResourceAssembler.class}) // Import assemblers for WebMvcTest +class FooControllerTest { @Autowired MockMvc mockMvc; @Test - public void shouldGet() throws Exception { + void shouldGet() throws Exception { mockMvc.perform(get("/foos/{id}", "cafe")) .andDo(print()) .andExpect(status().isOk()) @@ -39,8 +36,8 @@ public void shouldGet() throws Exception { } @Test - public void shouldCreate() throws Exception { - mockMvc.perform(post("/foos/") + void shouldCreate() throws Exception { + mockMvc.perform(post("/foos") .content("{\"uuid\":\"newFoo\",\"name\":\"Very New Foo\"}") .contentType("application/hal+json")) .andDo(print()) @@ -49,7 +46,7 @@ public void shouldCreate() throws Exception { } @Test - public void update() throws Exception { + void update() throws Exception { mockMvc.perform(put("/foos/{id}", "foo") .content("{\"uuid\":\"foo\",\"name\":\"Sample Foo\"}") .contentType("application/hal+json")) diff --git a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/pom.xml b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/pom.xml new file mode 100644 index 0000000..d636077 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + com.devskiller.friendly-id + spring-boot-jpa-demo + ${revision} + + + org.springframework.boot + spring-boot-starter-parent + 4.0.1 + + + + + 2.0.0-SNAPSHOT + UTF-8 + UTF-8 + 21 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + com.h2database + h2 + runtime + + + + + com.devskiller.friendly-id + friendly-id-spring-boot-starter + ${project.version} + + + + + com.devskiller.friendly-id + friendly-id-jpa + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + maven-compiler-plugin + 3.14.0 + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + diff --git a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/FriendlyIdJpaDemoApplication.java b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/FriendlyIdJpaDemoApplication.java new file mode 100644 index 0000000..fc8c27b --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/FriendlyIdJpaDemoApplication.java @@ -0,0 +1,95 @@ +package com.devskiller.friendly_id.sample.jpa; + +import java.math.BigDecimal; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +/** + * Spring Boot application demonstrating FriendlyId usage with JPA. + *

+ * This demo shows: + *

+ *
    + *
  • Using FriendlyId as entity ID (stored as UUID in database)
  • + *
  • Automatic conversion in REST endpoints via @PathVariable
  • + *
  • JSON serialization with FriendlyId strings
  • + *
  • H2 console for database inspection
  • + *
+ *

+ * Access points: + *

+ *
    + *
  • REST API: http://localhost:8080/api/products
  • + *
  • H2 Console: http://localhost:8080/h2-console (JDBC URL: jdbc:h2:mem:friendlyid_demo)
  • + *
+ */ +@SpringBootApplication +public class FriendlyIdJpaDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(FriendlyIdJpaDemoApplication.class, args); + } + + /** + * Initialize database with sample data. + */ + @Bean + public CommandLineRunner initData(ProductRepository repository) { + return args -> { + System.out.println(""" + + ======================================== + Initializing demo products... + ======================================== + """); + + var laptop = new Product( + "Laptop", + "High-performance laptop for developers", + new BigDecimal("1299.99"), + 15 + ); + repository.save(laptop); + System.out.println("Created product: Laptop with ID: " + laptop.getId()); + + var mouse = new Product( + "Wireless Mouse", + "Ergonomic wireless mouse", + new BigDecimal("29.99"), + 50 + ); + repository.save(mouse); + System.out.println("Created product: Wireless Mouse with ID: " + mouse.getId()); + + var keyboard = new Product( + "Mechanical Keyboard", + "RGB mechanical keyboard with Cherry MX switches", + new BigDecimal("149.99"), + 25 + ); + repository.save(keyboard); + System.out.println("Created product: Mechanical Keyboard with ID: " + keyboard.getId()); + + System.out.println(""" + + ======================================== + Demo ready! + ======================================== + REST API: http://localhost:8080/api/products + H2 Console: http://localhost:8080/h2-console + JDBC URL: jdbc:h2:mem:friendlyid_demo + Username: sa + Password: (empty) + ======================================== + + Try these commands: + curl http://localhost:8080/api/products + curl http://localhost:8080/api/products/%s + + """.formatted(laptop.getId())); + }; + } +} diff --git a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/Product.java b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/Product.java new file mode 100644 index 0000000..b33e281 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/Product.java @@ -0,0 +1,91 @@ +package com.devskiller.friendly_id.sample.jpa; + +import java.math.BigDecimal; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * Product entity demonstrating FriendlyId usage with JPA. + *

+ * The FriendlyId is automatically converted to/from UUID in the database + * thanks to the FriendlyIdConverter with @Converter(autoApply = true). + *

+ */ +@Entity +@Table(name = "products") +public class Product { + + @Id + private FriendlyId id; + + @Column(nullable = false) + private String name; + + @Column(length = 1000) + private String description; + + @Column(nullable = false) + private BigDecimal price; + + @Column(nullable = false) + private Integer stock; + + // Default constructor required by JPA + protected Product() { + } + + public Product(String name, String description, BigDecimal price, Integer stock) { + this.id = FriendlyId.random(); + this.name = name; + this.description = description; + this.price = price; + this.stock = stock; + } + + // Getters and setters + + public FriendlyId getId() { + return id; + } + + public void setId(FriendlyId id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public Integer getStock() { + return stock; + } + + public void setStock(Integer stock) { + this.stock = stock; + } +} diff --git a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductController.java b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductController.java new file mode 100644 index 0000000..d8052b3 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductController.java @@ -0,0 +1,143 @@ +package com.devskiller.friendly_id.sample.jpa; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * REST controller demonstrating FriendlyId usage with Spring MVC and JPA. + *

+ * Key features demonstrated: + *

+ *
    + *
  • @PathVariable automatically converts FriendlyId string to FriendlyId value object
  • + *
  • Response JSON contains FriendlyId as Base62 string (thanks to Jackson integration)
  • + *
  • Database stores UUID internally (thanks to JPA converter)
  • + *
+ *

+ * Example URLs: + *

+ *
+ * GET  /api/products                    - List all products
+ * GET  /api/products/5wbwf6yUxVBcr48   - Get product by FriendlyId
+ * POST /api/products                    - Create new product
+ * PUT  /api/products/5wbwf6yUxVBcr48   - Update product
+ * DELETE /api/products/5wbwf6yUxVBcr48 - Delete product
+ * 
+ */ +@RestController +@RequestMapping("/api/products") +public class ProductController { + + private final ProductRepository productRepository; + + public ProductController(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + /** + * Get all products. + *

+ * Response: JSON array with FriendlyId strings instead of UUIDs. + *

+ */ + @GetMapping + public List getAllProducts() { + return productRepository.findAll(); + } + + /** + * Get product by FriendlyId. + *

+ * Example: GET /api/products/5wbwf6yUxVBcr48AMbz9cb + *

+ *

+ * The FriendlyId string from URL is automatically converted to FriendlyId value object + * by Spring's StringToFriendlyIdConverter. + *

+ */ + @GetMapping("/{id}") + public ResponseEntity getProductById(@PathVariable FriendlyId id) { + return productRepository.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * Create new product. + *

+ * Request body should NOT include 'id' - it will be generated automatically. + *

+ *

+ * Example request: + *

+ *
+	 * {
+	 *   "name": "Laptop",
+	 *   "description": "High-performance laptop",
+	 *   "price": 1299.99,
+	 *   "stock": 10
+	 * }
+	 * 
+ */ + @PostMapping + public ResponseEntity createProduct(@RequestBody ProductRequest request) { + Product product = new Product( + request.name(), + request.description(), + request.price(), + request.stock() + ); + Product savedProduct = productRepository.save(product); + return ResponseEntity.status(HttpStatus.CREATED).body(savedProduct); + } + + /** + * Update existing product. + *

+ * Example: PUT /api/products/5wbwf6yUxVBcr48AMbz9cb + *

+ */ + @PutMapping("/{id}") + public ResponseEntity updateProduct( + @PathVariable FriendlyId id, + @RequestBody ProductRequest request) { + + return productRepository.findById(id) + .map(existingProduct -> { + existingProduct.setName(request.name()); + existingProduct.setDescription(request.description()); + existingProduct.setPrice(request.price()); + existingProduct.setStock(request.stock()); + Product updatedProduct = productRepository.save(existingProduct); + return ResponseEntity.ok(updatedProduct); + }) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * Delete product by FriendlyId. + *

+ * Example: DELETE /api/products/5wbwf6yUxVBcr48AMbz9cb + *

+ */ + @DeleteMapping("/{id}") + public ResponseEntity deleteProduct(@PathVariable FriendlyId id) { + if (productRepository.existsById(id)) { + productRepository.deleteById(id); + return ResponseEntity.noContent().build(); + } + return ResponseEntity.notFound().build(); + } +} diff --git a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductRepository.java b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductRepository.java new file mode 100644 index 0000000..f87548d --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductRepository.java @@ -0,0 +1,24 @@ +package com.devskiller.friendly_id.sample.jpa; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * Spring Data JPA repository for Product entities. + *

+ * Note: The repository uses FriendlyId as the ID type, not UUID. + * Spring Data automatically handles the FriendlyId type. + *

+ */ +@Repository +public interface ProductRepository extends JpaRepository { + + /** + * Find product by name (case-insensitive). + */ + Optional findByNameIgnoreCase(String name); +} diff --git a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductRequest.java b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductRequest.java new file mode 100644 index 0000000..06d7269 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductRequest.java @@ -0,0 +1,14 @@ +package com.devskiller.friendly_id.sample.jpa; + +import java.math.BigDecimal; + +/** + * DTO for creating/updating products. + */ +public record ProductRequest( + String name, + String description, + BigDecimal price, + Integer stock +) { +} diff --git a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/resources/application.properties b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/resources/application.properties new file mode 100644 index 0000000..5be0a6c --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/resources/application.properties @@ -0,0 +1,25 @@ +# Server Configuration +server.port=8090 + +# H2 Database Configuration +spring.datasource.url=jdbc:h2:mem:friendlyid_demo +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +# JPA/Hibernate Configuration +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# H2 Console (accessible at http://localhost:8080/h2-console) +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# Logging +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +logging.level.com.devskiller.friendly_id=DEBUG +logging.level.org.springframework.web=DEBUG +logging.level.com.fasterxml.jackson=DEBUG diff --git a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClientIntegrationTest.java b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClientIntegrationTest.java new file mode 100644 index 0000000..d6a8977 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClientIntegrationTest.java @@ -0,0 +1,92 @@ +package com.devskiller.friendly_id.sample.jpa; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.devskiller.friendly_id.type.FriendlyId; + +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Integration test demonstrating FriendlyId usage with JPA. + *

+ * This test shows that FriendlyId works seamlessly across the entire stack: + *

+ *
    + *
  1. Entity stored in database with FriendlyId as UUID
  2. + *
  3. REST controller accepts FriendlyId in @PathVariable
  4. + *
  5. JSON serialization converts FriendlyId to/from string
  6. + *
+ */ +@SpringBootTest +@AutoConfigureMockMvc +class ProductClientIntegrationTest { + + @Autowired + private ProductRepository repository; + + @Autowired + private MockMvc mockMvc; + + private Product testProduct; + + @BeforeEach + void setUp() { + repository.deleteAll(); + + testProduct = new Product( + "Test Product", + "Product for integration testing", + new BigDecimal("99.99"), + 10 + ); + repository.save(testProduct); + } + + @Test + void shouldRetrieveAllProducts() throws Exception { + mockMvc.perform(get("/api/products") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].name", is("Test Product"))); + } + + @Test + void shouldRetrieveProductByFriendlyId() throws Exception { + FriendlyId productId = testProduct.getId(); + + mockMvc.perform(get("/api/products/{id}", productId.toString()) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", is(productId.toString()))) + .andExpect(jsonPath("$.name", is("Test Product"))) + .andExpect(jsonPath("$.description", is("Product for integration testing"))) + .andExpect(jsonPath("$.price", is(99.99))) + .andExpect(jsonPath("$.stock", is(10))); + } + + @Test + void shouldHandleFriendlyIdConversionInUrlPath() throws Exception { + // This test verifies that FriendlyId in @PathVariable is correctly: + // 1. Parsed from string by Spring MVC converter + // 2. Used to query the database via JPA converter + // 3. Serialized to JSON string in response + + mockMvc.perform(get("/api/products/{id}", testProduct.getId().toString()) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", matchesPattern("[0-9A-Za-z]{21,22}"))); + } +} diff --git a/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml b/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml index 2df02d2..7597461 100644 --- a/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml @@ -5,19 +5,20 @@ com.devskiller.friendly-id spring-boot-simple - 1.1.1-SNAPSHOT + ${revision} org.springframework.boot spring-boot-starter-parent - 2.2.2.RELEASE + 4.0.1 + 2.0.0-SNAPSHOT UTF-8 UTF-8 - 1.8 + 21 @@ -36,6 +37,11 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-webmvc-test + test + @@ -46,19 +52,14 @@
maven-compiler-plugin - 3.8.0 - - - org.apache.maven.plugins - maven-deploy-plugin + 3.14.0 - true + true - org.jacoco - jacoco-maven-plugin - 0.8.3 + org.apache.maven.plugins + maven-deploy-plugin true diff --git a/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Application.java b/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Application.java index ab35f7a..1b36034 100644 --- a/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Application.java +++ b/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Application.java @@ -6,8 +6,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.devskiller.friendly_id.type.FriendlyId; + @RestController @SpringBootApplication public class Application { @@ -16,11 +21,28 @@ public static void main(String[] args) { SpringApplication.run(Application.class, args); } - @GetMapping("/bars/{id}") - Bar getBar(@PathVariable UUID id) { - Bar bar = new Bar(); - bar.setId(id); - return bar; + @GetMapping("/items/{id}") + Item getItem(@PathVariable UUID id) { + return new Item(id, id, id, FriendlyId.of(id)); + } + + @PostMapping("/items") + Item createItem(@RequestBody Item item) { + if (item.id() == null) { + var uuid = UUID.randomUUID(); + return new Item(uuid, uuid, uuid, FriendlyId.of(uuid)); + } + return item; } + @GetMapping("/items") + Item getItemByParam(@RequestParam UUID id) { + return new Item(id, id, id, FriendlyId.of(id)); + } + + @GetMapping("/items/by-friendly-id") + Item getItemByFriendlyIdParam(@RequestParam FriendlyId id) { + var uuid = id.uuid(); + return new Item(uuid, uuid, uuid, id); + } } diff --git a/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Bar.java b/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Bar.java deleted file mode 100644 index 7d44099..0000000 --- a/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Bar.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.devskiller.friendly_id.sample.simple; - -import java.util.UUID; - -public class Bar { - - private UUID id; - - public UUID getId() { - return id; - } - - public void setId(UUID id) { - this.id = id; - } -} diff --git a/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Item.java b/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Item.java new file mode 100644 index 0000000..b4ad243 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Item.java @@ -0,0 +1,23 @@ +package com.devskiller.friendly_id.sample.simple; + +import java.util.UUID; + +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * Example record demonstrating different UUID serialization formats. + * + * @param id UUID serialized as FriendlyId string (default behavior) + * @param rawId UUID serialized as raw UUID string + * @param friendlyUuid UUID explicitly serialized as FriendlyId string + * @param friendlyId FriendlyId value object type + */ +public record Item( + UUID id, + @IdFormat(FriendlyIdFormat.RAW) UUID rawId, + @IdFormat(FriendlyIdFormat.URL62) UUID friendlyUuid, + FriendlyId friendlyId +) { +} diff --git a/friendly-id-samples/friendly-id-spring-boot-simple/src/test/java/com/devskiller/friendly_id/sample/simple/ApplicationTest.java b/friendly-id-samples/friendly-id-spring-boot-simple/src/test/java/com/devskiller/friendly_id/sample/simple/ApplicationTest.java index f3406a0..1886626 100644 --- a/friendly-id-samples/friendly-id-spring-boot-simple/src/test/java/com/devskiller/friendly_id/sample/simple/ApplicationTest.java +++ b/friendly-id-samples/friendly-id-spring-boot-simple/src/test/java/com/devskiller/friendly_id/sample/simple/ApplicationTest.java @@ -2,34 +2,149 @@ import java.util.UUID; -import org.junit.Test; -import org.junit.runner.RunWith; - +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.devskiller.friendly_id.FriendlyId; -import static org.assertj.core.api.BDDAssertions.then; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -@RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class ApplicationTest { +@SpringBootTest +@AutoConfigureMockMvc +class ApplicationTest { @Autowired - private TestRestTemplate restTemplate; + private MockMvc mockMvc; + @Test + void shouldAcceptFriendlyIdAsPathVariable() throws Exception { + // given + UUID uuid = UUID.randomUUID(); + String friendlyId = FriendlyId.toFriendlyId(uuid); + String rawUuid = uuid.toString(); + + // when/then + mockMvc.perform(get("/items/{id}", friendlyId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", is(friendlyId))) + .andExpect(jsonPath("$.rawId", is(rawUuid))) + .andExpect(jsonPath("$.friendlyUuid", is(friendlyId))) + .andExpect(jsonPath("$.friendlyId", is(friendlyId))); + } @Test - public void shouldSerialize() { + void shouldAcceptUuidAsPathVariable() throws Exception { + // given + UUID uuid = UUID.randomUUID(); + String friendlyId = FriendlyId.toFriendlyId(uuid); + String rawUuid = uuid.toString(); + // when/then - using raw UUID as path variable + mockMvc.perform(get("/items/{id}", rawUuid) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", is(friendlyId))) + .andExpect(jsonPath("$.rawId", is(rawUuid))) + .andExpect(jsonPath("$.friendlyUuid", is(friendlyId))) + .andExpect(jsonPath("$.friendlyId", is(friendlyId))); + } + + @Test + void shouldDeserializeAndSerialize() throws Exception { // given UUID uuid = UUID.randomUUID(); + String friendlyId = FriendlyId.toFriendlyId(uuid); + String json = """ + {"id": "%s"} + """.formatted(friendlyId); + + // when/then + mockMvc.perform(post("/items") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", is(friendlyId))); + } - // expect - Bar entity = restTemplate.getForEntity("/bars/{id}", Bar.class, uuid).getBody(); + @Test + void shouldGenerateAllIdsWhenNotProvided() throws Exception { + // given + String json = "{}"; - then(entity.getId()).isEqualTo(uuid); + // when/then + mockMvc.perform(post("/items") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", notNullValue())) + .andExpect(jsonPath("$.rawId", notNullValue())) + .andExpect(jsonPath("$.friendlyUuid", notNullValue())) + .andExpect(jsonPath("$.friendlyId", notNullValue())); + } + + @Test + void shouldAcceptFriendlyIdAsRequestParam() throws Exception { + // given + UUID uuid = UUID.randomUUID(); + String friendlyId = FriendlyId.toFriendlyId(uuid); + String rawUuid = uuid.toString(); + + // when/then - using FriendlyId as ?id=xxx + mockMvc.perform(get("/items") + .param("id", friendlyId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", is(friendlyId))) + .andExpect(jsonPath("$.rawId", is(rawUuid))); + } + + @Test + void shouldAcceptUuidAsRequestParam() throws Exception { + // given + UUID uuid = UUID.randomUUID(); + String friendlyId = FriendlyId.toFriendlyId(uuid); + String rawUuid = uuid.toString(); + + // when/then - using raw UUID as ?id=xxx + mockMvc.perform(get("/items") + .param("id", rawUuid) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", is(friendlyId))) + .andExpect(jsonPath("$.rawId", is(rawUuid))); + } + + @Test + void shouldAcceptFriendlyIdTypeAsRequestParam() throws Exception { + // given + UUID uuid = UUID.randomUUID(); + String friendlyId = FriendlyId.toFriendlyId(uuid); + String rawUuid = uuid.toString(); + // when/then - using FriendlyId string with @RequestParam FriendlyId type + mockMvc.perform(get("/items/by-friendly-id") + .param("id", friendlyId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", is(friendlyId))) + .andExpect(jsonPath("$.rawId", is(rawUuid))); } } diff --git a/friendly-id-samples/friendly-id-spring-boot3-simple/pom.xml b/friendly-id-samples/friendly-id-spring-boot3-simple/pom.xml new file mode 100644 index 0000000..3b86142 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot3-simple/pom.xml @@ -0,0 +1,67 @@ + + + + 4.0.0 + + com.devskiller.friendly-id + spring-boot3-simple + ${revision} + + FriendlyId Spring Boot 3 Sample + Sample application demonstrating FriendlyId with Spring Boot 3 and Jackson 2.x + + + org.springframework.boot + spring-boot-starter-parent + 3.4.1 + + + + + 2.0.0-SNAPSHOT + UTF-8 + UTF-8 + 21 + + + + + org.springframework.boot + spring-boot-starter-web + + + com.devskiller.friendly-id + friendly-id-jackson2-datatype + ${project.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + maven-compiler-plugin + 3.14.0 + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + diff --git a/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Application.java b/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Application.java new file mode 100644 index 0000000..0c0c88d --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Application.java @@ -0,0 +1,37 @@ +package com.devskiller.friendly_id.sample.spring3; + +import java.util.UUID; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.devskiller.friendly_id.type.FriendlyId; + +@RestController +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @GetMapping("/items/{id}") + Item getItem(@PathVariable UUID id) { + return new Item(id, id, id, FriendlyId.of(id)); + } + + @PostMapping("/items") + Item createItem(@RequestBody Item item) { + if (item.id() == null) { + var uuid = UUID.randomUUID(); + return new Item(uuid, uuid, uuid, FriendlyId.of(uuid)); + } + return item; + } + +} diff --git a/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/FriendlyIdConfig.java b/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/FriendlyIdConfig.java new file mode 100644 index 0000000..69d28e3 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/FriendlyIdConfig.java @@ -0,0 +1,43 @@ +package com.devskiller.friendly_id.sample.spring3; + +import java.util.UUID; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import com.devskiller.friendly_id.FriendlyId; +import com.devskiller.friendly_id.jackson2.FriendlyIdJackson2Module; + +@Configuration +public class FriendlyIdConfig implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new StringToUuidConverter()); + registry.addConverter(new UuidToStringConverter()); + } + + @Bean + FriendlyIdJackson2Module friendlyIdModule() { + return new FriendlyIdJackson2Module(); + } + + public static class StringToUuidConverter implements Converter { + + @Override + public UUID convert(String id) { + return FriendlyId.toUuid(id); + } + } + + public static class UuidToStringConverter implements Converter { + + @Override + public String convert(UUID id) { + return FriendlyId.toFriendlyId(id); + } + } +} diff --git a/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Item.java b/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Item.java new file mode 100644 index 0000000..277b83c --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Item.java @@ -0,0 +1,23 @@ +package com.devskiller.friendly_id.sample.spring3; + +import java.util.UUID; + +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * Example record demonstrating different UUID serialization formats. + * + * @param id UUID serialized as FriendlyId string (default behavior) + * @param rawId UUID serialized as raw UUID string + * @param friendlyUuid UUID explicitly serialized as FriendlyId string + * @param friendlyId FriendlyId value object type + */ +public record Item( + UUID id, + @IdFormat(FriendlyIdFormat.RAW) UUID rawId, + @IdFormat(FriendlyIdFormat.URL62) UUID friendlyUuid, + FriendlyId friendlyId +) { +} diff --git a/friendly-id-samples/friendly-id-spring-boot3-simple/src/test/java/com/devskiller/friendly_id/sample/spring3/ApplicationTest.java b/friendly-id-samples/friendly-id-spring-boot3-simple/src/test/java/com/devskiller/friendly_id/sample/spring3/ApplicationTest.java new file mode 100644 index 0000000..7cfec84 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot3-simple/src/test/java/com/devskiller/friendly_id/sample/spring3/ApplicationTest.java @@ -0,0 +1,81 @@ +package com.devskiller.friendly_id.sample.spring3; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.devskiller.friendly_id.FriendlyId; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +class ApplicationTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void shouldSerializeAllIdFormats() throws Exception { + // given + UUID uuid = UUID.randomUUID(); + String friendlyId = FriendlyId.toFriendlyId(uuid); + String rawUuid = uuid.toString(); + + // when/then + mockMvc.perform(get("/items/{id}", friendlyId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", is(friendlyId))) + .andExpect(jsonPath("$.rawId", is(rawUuid))) + .andExpect(jsonPath("$.friendlyUuid", is(friendlyId))) + .andExpect(jsonPath("$.friendlyId", is(friendlyId))); + } + + @Test + void shouldDeserializeAndSerialize() throws Exception { + // given + UUID uuid = UUID.randomUUID(); + String friendlyId = FriendlyId.toFriendlyId(uuid); + String json = """ + {"id": "%s"} + """.formatted(friendlyId); + + // when/then + mockMvc.perform(post("/items") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", is(friendlyId))); + } + + @Test + void shouldGenerateAllIdsWhenNotProvided() throws Exception { + // given + String json = "{}"; + + // when/then + mockMvc.perform(post("/items") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", notNullValue())) + .andExpect(jsonPath("$.rawId", notNullValue())) + .andExpect(jsonPath("$.friendlyUuid", notNullValue())) + .andExpect(jsonPath("$.friendlyId", notNullValue())); + } +} diff --git a/friendly-id-samples/pom.xml b/friendly-id-samples/pom.xml index 9f7a0f4..d423c64 100644 --- a/friendly-id-samples/pom.xml +++ b/friendly-id-samples/pom.xml @@ -4,18 +4,22 @@ friendly-id-samples pom + FriendlyId Samples + Sample applications demonstrating FriendlyId usage com.devskiller.friendly-id friendly-id-project - 1.1.1-SNAPSHOT + ${revision} .. friendly-id-spring-boot-simple + friendly-id-spring-boot3-simple friendly-id-spring-boot-customized friendly-id-spring-boot-hateos + friendly-id-spring-boot-jpa-demo friendly-id-contracts @@ -27,6 +31,19 @@ true + + maven-deploy-plugin + + true + + + + org.sonatype.central + central-publishing-maven-plugin + + true + +
diff --git a/friendly-id-spring-boot-starter/pom.xml b/friendly-id-spring-boot-starter/pom.xml index 4323276..18f3d2b 100644 --- a/friendly-id-spring-boot-starter/pom.xml +++ b/friendly-id-spring-boot-starter/pom.xml @@ -1,21 +1,30 @@ + 4.0.0 + com.devskiller.friendly-id friendly-id-project - 1.1.1-SNAPSHOT + ${revision} .. - 4.0.0 friendly-id-spring-boot-starter + FriendlyId Spring Boot Starter + Spring Boot starter for FriendlyId - auto-configuration for easy integration + com.devskiller.friendly-id friendly-id-spring-boot ${project.version} + + com.devskiller.friendly-id + friendly-id-jackson-datatype + ${project.version} + org.springframework.boot spring-boot-starter diff --git a/friendly-id-spring-boot-starter/src/main/java/com/devskiller/friendly_id/boot/FriendlyIdAutoConfiguration.java b/friendly-id-spring-boot-starter/src/main/java/com/devskiller/friendly_id/boot/FriendlyIdAutoConfiguration.java index de81846..3a30185 100644 --- a/friendly-id-spring-boot-starter/src/main/java/com/devskiller/friendly_id/boot/FriendlyIdAutoConfiguration.java +++ b/friendly-id-spring-boot-starter/src/main/java/com/devskiller/friendly_id/boot/FriendlyIdAutoConfiguration.java @@ -1,12 +1,25 @@ package com.devskiller.friendly_id.boot; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.context.annotation.Configuration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import com.devskiller.friendly_id.spring.EnableFriendlyId; -@Configuration -@ConditionalOnExpression("${com.devskiller.friendly_id.auto:true}") +/** + * Auto-configuration for FriendlyId integration with Spring Boot. + *

+ * Automatically enables FriendlyId converters and Jackson module when Spring Boot is detected. + * Can be disabled by setting {@code com.devskiller.friendly-id.enabled=false} in application properties. + */ +@AutoConfiguration +@ConditionalOnWebApplication +@ConditionalOnProperty( + prefix = "com.devskiller.friendly-id", + name = "enabled", + havingValue = "true", + matchIfMissing = true +) @EnableFriendlyId public class FriendlyIdAutoConfiguration { diff --git a/friendly-id-spring-boot-starter/src/main/resources/META-INF/spring.factories b/friendly-id-spring-boot-starter/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 561642c..0000000 --- a/friendly-id-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration = com.devskiller.friendly_id.boot.FriendlyIdAutoConfiguration \ No newline at end of file diff --git a/friendly-id-spring-boot/pom.xml b/friendly-id-spring-boot/pom.xml index 8b8f357..da30023 100644 --- a/friendly-id-spring-boot/pom.xml +++ b/friendly-id-spring-boot/pom.xml @@ -1,30 +1,36 @@ + 4.0.0 + com.devskiller.friendly-id friendly-id-project - 1.1.1-SNAPSHOT + ${revision} .. - 4.0.0 friendly-id-spring-boot + FriendlyId Spring Boot + Spring Boot integration for FriendlyId - provides converters and Jackson module configuration + - - org.springframework.boot - spring-boot-starter - provided - org.springframework.boot spring-boot-starter-web + provided com.devskiller.friendly-id friendly-id-jackson-datatype ${project.version} + + + tools.jackson.core + jackson-databind + provided + \ No newline at end of file diff --git a/friendly-id-spring-boot/src/main/java/com/devskiller/friendly_id/spring/EnableFriendlyId.java b/friendly-id-spring-boot/src/main/java/com/devskiller/friendly_id/spring/EnableFriendlyId.java index a85f856..f6b3810 100644 --- a/friendly-id-spring-boot/src/main/java/com/devskiller/friendly_id/spring/EnableFriendlyId.java +++ b/friendly-id-spring-boot/src/main/java/com/devskiller/friendly_id/spring/EnableFriendlyId.java @@ -1,5 +1,6 @@ package com.devskiller.friendly_id.spring; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -7,8 +8,33 @@ import org.springframework.context.annotation.Import; +/** + * Enable FriendlyId support in Spring MVC applications. + *

+ * Add this annotation to a {@code @Configuration} class to enable automatic conversion + * between FriendlyId strings and UUIDs in: + *

    + *
  • Path variables ({@code @PathVariable UUID id})
  • + *
  • Request parameters ({@code @RequestParam UUID id})
  • + *
  • JSON request/response bodies (via Jackson)
  • + *
+ *

+ * Example usage: + *

+ * @Configuration
+ * @EnableFriendlyId
+ * public class WebConfig {
+ * }
+ * 
+ *

+ * Note: When using {@code spring-boot-starter-friendly-id}, this configuration + * is applied automatically and this annotation is not required. + * + * @see FriendlyIdConfiguration + */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) +@Documented @Import(FriendlyIdConfiguration.class) public @interface EnableFriendlyId { diff --git a/friendly-id-spring-boot/src/main/java/com/devskiller/friendly_id/spring/FriendlyIdConfiguration.java b/friendly-id-spring-boot/src/main/java/com/devskiller/friendly_id/spring/FriendlyIdConfiguration.java index 263e61a..56fc78a 100644 --- a/friendly-id-spring-boot/src/main/java/com/devskiller/friendly_id/spring/FriendlyIdConfiguration.java +++ b/friendly-id-spring-boot/src/main/java/com/devskiller/friendly_id/spring/FriendlyIdConfiguration.java @@ -2,46 +2,45 @@ import java.util.UUID; -import com.fasterxml.jackson.databind.Module; +import tools.jackson.databind.JacksonModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.convert.converter.Converter; import org.springframework.format.FormatterRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import com.devskiller.friendly_id.FriendlyId; import com.devskiller.friendly_id.jackson.FriendlyIdModule; +import static com.devskiller.friendly_id.FriendlyId.toFriendlyId; +import static com.devskiller.friendly_id.FriendlyId.toUuid; + +/** + * Configuration for FriendlyId integration with Spring MVC. + *

+ * This configuration: + *

    + *
  • Registers converters for automatic String ⇄ UUID conversion in path variables and request parameters
  • + *
  • Registers Jackson module for JSON serialization/deserialization of UUIDs as FriendlyIds
  • + *
+ *

+ * Enable this configuration by adding {@link EnableFriendlyId @EnableFriendlyId} to your configuration class, + * or use the spring-boot-starter for automatic configuration. + */ @Configuration public class FriendlyIdConfiguration implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { - registry.addConverter(new StringToUuidConverter()); - registry.addConverter(new UuidToStringConverter()); + registry.addConverter(String.class, UUID.class, id -> toUuid(id)); + registry.addConverter(UUID.class, String.class, id -> toFriendlyId(id)); + registry.addConverter(String.class, com.devskiller.friendly_id.type.FriendlyId.class, + com.devskiller.friendly_id.type.FriendlyId::fromString); + registry.addConverter(com.devskiller.friendly_id.type.FriendlyId.class, String.class, + com.devskiller.friendly_id.type.FriendlyId::toString); } @Bean - public Module friendlyIdModule() { + public JacksonModule friendlyIdModule() { return new FriendlyIdModule(); } - - //FIXME: make this public - public static class StringToUuidConverter implements Converter { - - @Override - public UUID convert(String id) { - return FriendlyId.toUuid(id); - } - } - - - public static class UuidToStringConverter implements Converter { - - @Override - public String convert(UUID id) { - return FriendlyId.toFriendlyId(id); - } - } } diff --git a/friendly-id/pom.xml b/friendly-id/pom.xml index abfd26e..29d57e9 100644 --- a/friendly-id/pom.xml +++ b/friendly-id/pom.xml @@ -6,16 +6,19 @@ com.devskiller.friendly-id friendly-id-project - 1.1.1-SNAPSHOT + ${revision} .. friendly-id + FriendlyId Core + Core library for converting UUIDs to URL-friendly Base62-encoded IDs + - junit - junit + org.junit.jupiter + junit-jupiter test @@ -23,11 +26,6 @@ assertj-core test - - io.vavr - vavr-test - test - diff --git a/friendly-id/src/main/java/com/devskiller/friendly_id/Base62.java b/friendly-id/src/main/java/com/devskiller/friendly_id/Base62.java index c6a1c66..3dbec63 100644 --- a/friendly-id/src/main/java/com/devskiller/friendly_id/Base62.java +++ b/friendly-id/src/main/java/com/devskiller/friendly_id/Base62.java @@ -38,7 +38,7 @@ static String encode(BigInteger number) { int digit = divmod[1].intValue(); result.insert(0, DIGITS.charAt(digit)); } - return (result.length() == 0) ? DIGITS.substring(0, 1) : result.toString(); + return (result.isEmpty()) ? DIGITS.substring(0, 1) : result.toString(); } private static BigInteger throwIllegalArgumentException(String format, Object... args) { @@ -59,7 +59,7 @@ static BigInteger decode(final String string) { static BigInteger decode(final String string, int bitLimit) { requireNonNull(string, "Decoded string must not be null"); - if (string.length() == 0) { + if (string.isEmpty()) { return throwIllegalArgumentException("String '%s' must not be empty", string); } @@ -79,7 +79,7 @@ static BigInteger decode(final String string, int bitLimit) { } - private static BiFunction charAt = (string, index) -> + private static final BiFunction charAt = (string, index) -> DIGITS.indexOf(string.charAt(string.length() - index - 1)); } \ No newline at end of file diff --git a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdFormat.java b/friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIdFormat.java similarity index 78% rename from friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdFormat.java rename to friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIdFormat.java index 2fb1985..6e46580 100644 --- a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdFormat.java +++ b/friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIdFormat.java @@ -1,4 +1,4 @@ -package com.devskiller.friendly_id.jackson; +package com.devskiller.friendly_id; /** * Friendly ID format diff --git a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/IdFormat.java b/friendly-id/src/main/java/com/devskiller/friendly_id/IdFormat.java similarity index 86% rename from friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/IdFormat.java rename to friendly-id/src/main/java/com/devskiller/friendly_id/IdFormat.java index 999c366..5094a41 100644 --- a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/IdFormat.java +++ b/friendly-id/src/main/java/com/devskiller/friendly_id/IdFormat.java @@ -1,4 +1,4 @@ -package com.devskiller.friendly_id.jackson; +package com.devskiller.friendly_id; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/friendly-id/src/main/java/com/devskiller/friendly_id/type/FriendlyId.java b/friendly-id/src/main/java/com/devskiller/friendly_id/type/FriendlyId.java new file mode 100644 index 0000000..9fd70d3 --- /dev/null +++ b/friendly-id/src/main/java/com/devskiller/friendly_id/type/FriendlyId.java @@ -0,0 +1,135 @@ +package com.devskiller.friendly_id.type; + +import java.io.Serializable; +import java.util.Objects; +import java.util.UUID; + +/** + * Value object representing a FriendlyId that wraps a UUID. + *

+ * This class provides a memory-efficient way to work with FriendlyIds in your domain model + * while maintaining the compact UUID representation internally. The FriendlyId string + * representation is only computed when needed (e.g., for serialization or toString()). + *

+ *

+ * This type is designed to be used with: + *

+ *
    + *
  • jOOQ converters for database mapping
  • + *
  • JPA AttributeConverters for entity mapping
  • + *
  • Jackson serializers/deserializers for JSON
  • + *
  • Spring MVC converters for request parameters
  • + *
+ * + *

Memory Efficiency

+ *

+ * Storing FriendlyId as a value object with UUID internally is more memory-efficient + * than storing the String representation: + *

+ *
    + *
  • UUID: 16 bytes
  • + *
  • FriendlyId object: ~28 bytes (16 bytes UUID + ~12 bytes object header)
  • + *
  • String: ~40-50 bytes (depending on FriendlyId length)
  • + *
+ * + *

Usage Example

+ *
{@code
+ * // Create from UUID
+ * UUID uuid = UUID.randomUUID();
+ * FriendlyId id = FriendlyId.of(uuid);
+ *
+ * // Create from String
+ * FriendlyId id = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb");
+ *
+ * // Create random
+ * FriendlyId id = FriendlyId.random();
+ *
+ * // Get UUID
+ * UUID uuid = id.uuid();
+ *
+ * // Get FriendlyId string (computed on demand)
+ * String friendlyIdString = id.toString();
+ * }
+ * + * @since 1.1.1 + */ +public final class FriendlyId implements Serializable, Comparable { + + private static final long serialVersionUID = 1L; + + private final UUID uuid; + + private FriendlyId(UUID uuid) { + this.uuid = Objects.requireNonNull(uuid, "UUID cannot be null"); + } + + /** + * Creates a FriendlyId from a UUID. + * + * @param uuid the UUID to wrap, must not be null + * @return a new FriendlyId instance + * @throws NullPointerException if uuid is null + */ + public static FriendlyId of(UUID uuid) { + return new FriendlyId(uuid); + } + + /** + * Creates a FriendlyId from a FriendlyId string representation. + * + * @param friendlyId the FriendlyId string to decode, must not be null + * @return a new FriendlyId instance + * @throws NullPointerException if friendlyId is null + * @throws IllegalArgumentException if friendlyId is not a valid FriendlyId + */ + public static FriendlyId fromString(String friendlyId) { + Objects.requireNonNull(friendlyId, "FriendlyId string cannot be null"); + return new FriendlyId(com.devskiller.friendly_id.FriendlyId.toUuid(friendlyId)); + } + + /** + * Creates a random FriendlyId. + * + * @return a new random FriendlyId instance + */ + public static FriendlyId random() { + return new FriendlyId(UUID.randomUUID()); + } + + /** + * Returns the underlying UUID. + * + * @return the UUID + */ + public UUID uuid() { + return uuid; + } + + /** + * Returns the FriendlyId string representation. + *

+ * The string is computed on demand from the internal UUID. + *

+ * + * @return the FriendlyId string + */ + @Override + public String toString() { + return com.devskiller.friendly_id.FriendlyId.toFriendlyId(uuid); + } + + @Override + public boolean equals(Object o) { + return this == o || (o instanceof FriendlyId that && uuid.equals(that.uuid)); + } + + @Override + public int hashCode() { + return uuid.hashCode(); + } + + @Override + public int compareTo(FriendlyId other) { + return this.uuid.compareTo(other.uuid); + } +} diff --git a/friendly-id/src/main/java/com/devskiller/friendly_id/type/package-info.java b/friendly-id/src/main/java/com/devskiller/friendly_id/type/package-info.java new file mode 100644 index 0000000..0a9a83e --- /dev/null +++ b/friendly-id/src/main/java/com/devskiller/friendly_id/type/package-info.java @@ -0,0 +1,11 @@ +/** + * Value objects for FriendlyId domain model. + *

+ * This package provides type-safe, memory-efficient value objects for working with FriendlyIds + * in domain models and persistence layers. + *

+ * + * @see com.devskiller.friendly_id.type.FriendlyId + * @since 1.1.1 + */ +package com.devskiller.friendly_id.type; diff --git a/friendly-id/src/test/java/com/devskiller/friendly_id/AnalyzeGeneratedIdsTest.java b/friendly-id/src/test/java/com/devskiller/friendly_id/AnalyzeGeneratedIdsTest.java index 829c541..0eac895 100644 --- a/friendly-id/src/test/java/com/devskiller/friendly_id/AnalyzeGeneratedIdsTest.java +++ b/friendly-id/src/test/java/com/devskiller/friendly_id/AnalyzeGeneratedIdsTest.java @@ -1,21 +1,21 @@ package com.devskiller.friendly_id; +import org.junit.jupiter.api.Test; + import java.util.ArrayList; import java.util.IntSummaryStatistics; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; -import org.junit.Test; - import static org.assertj.core.api.Assertions.assertThat; -public class AnalyzeGeneratedIdsTest { +class AnalyzeGeneratedIdsTest { private List ids = new ArrayList<>(); @Test - public void analyzeGeneratedValueStatistics() { + void analyzeGeneratedValueStatistics() { for (int i = 0; i < 100_000; i++) { this.ids.add(Base62.encode(UuidConverter.toBigInteger(UUID.randomUUID()))); } diff --git a/friendly-id/src/test/java/com/devskiller/friendly_id/Base62Test.java b/friendly-id/src/test/java/com/devskiller/friendly_id/Base62Test.java index 266a8b9..8304f61 100644 --- a/friendly-id/src/test/java/com/devskiller/friendly_id/Base62Test.java +++ b/friendly-id/src/test/java/com/devskiller/friendly_id/Base62Test.java @@ -1,45 +1,48 @@ package com.devskiller.friendly_id; -import org.junit.Test; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.Random; import static com.devskiller.friendly_id.IdUtil.areEqualIgnoringLeadingZeros; -import static io.vavr.test.Property.def; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.util.Objects.areEqual; -public class Base62Test { +class Base62Test { @Test - public void decodingValuePrefixedWithZeros() { + void decodingValuePrefixedWithZeros() { assertThat(Base62.encode(Base62.decode("00001"))).isEqualTo("1"); assertThat(Base62.encode(Base62.decode("01001"))).isEqualTo("1001"); assertThat(Base62.encode(Base62.decode("00abcd"))).isEqualTo("abcd"); } @Test - public void shouldCheck128BitLimits() { + void shouldCheck128BitLimits() { assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> Base62.decode("1Vkp6axDWu5pI3q1xQO3oO0")); } - @Test - public void decodingIdShouldBeReversible() { - def("areEqualIgnoringLeadingZeros(Base62.toFriendlyId(Base62.toUuid(id)), id)") - .forAll(DataProvider.FRIENDLY_IDS) - .suchThat(id -> areEqualIgnoringLeadingZeros(Base62.encode(Base62.decode(id)), id)) - .check(24, 100_000) - .assertIsSatisfied(); + @RepeatedTest(1000) + void decodingIdShouldBeReversible() { + String id = generateRandomFriendlyId(); + String result = Base62.encode(Base62.decode(id)); + assertThat(areEqualIgnoringLeadingZeros(result, id)).isTrue(); } - @Test - public void encodingNumberShouldBeReversible() { - def("areEqualIgnoringLeadingZeros(Base62.toFriendlyId(Base62.toUuid(id)), id)") - .forAll(DataProvider.POSITIVE_BIG_INTEGERS) - .suchThat(bigInteger -> areEqual(Base62.decode(Base62.encode(bigInteger)), bigInteger) - ) - .check(-1, 100_000) - .assertIsSatisfied(); + @RepeatedTest(1000) + void encodingNumberShouldBeReversible() { + BigInteger bigInteger = new BigInteger(128, new Random()); + BigInteger result = Base62.decode(Base62.encode(bigInteger)); + assertThat(result).isEqualTo(bigInteger); + } + + private String generateRandomFriendlyId() { + Random random = new Random(); + BigInteger bigInt = new BigInteger(128, random); + return Base62.encode(bigInt); } } diff --git a/friendly-id/src/test/java/com/devskiller/friendly_id/BigIntegerPairingTest.java b/friendly-id/src/test/java/com/devskiller/friendly_id/BigIntegerPairingTest.java index ed15c2c..9b8b73f 100644 --- a/friendly-id/src/test/java/com/devskiller/friendly_id/BigIntegerPairingTest.java +++ b/friendly-id/src/test/java/com/devskiller/friendly_id/BigIntegerPairingTest.java @@ -1,21 +1,20 @@ package com.devskiller.friendly_id; -import java.math.BigInteger; -import java.util.Arrays; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; -import io.vavr.Tuple2; -import org.junit.Test; +import java.math.BigInteger; +import java.util.Random; import static com.devskiller.friendly_id.BigIntegerPairing.pair; import static com.devskiller.friendly_id.BigIntegerPairing.unpair; -import static io.vavr.test.Property.def; import static java.math.BigInteger.valueOf; import static org.assertj.core.api.Assertions.assertThat; -public class BigIntegerPairingTest { +class BigIntegerPairingTest { @Test - public void shouldPairTwoLongs() { + void shouldPairTwoLongs() { long x = 1; long y = 2; @@ -24,30 +23,27 @@ public void shouldPairTwoLongs() { assertThat(unpair(z)).contains(valueOf(x), valueOf(y)); } - @Test - public void resultOfPairingShouldBePositive() { - def("pair(longs).signum() > 0") - .forAll(DataProvider.LONG_PAIRS) - .suchThat(longs -> makePair(longs).signum() > 0) - .check(-1, 100_000) - .assertIsSatisfied(); - } + @RepeatedTest(1000) + void resultOfPairingShouldBePositive() { + Random random = new Random(); + long x = random.nextLong(); + long y = random.nextLong(); - private BigInteger makePair(Tuple2 longs) { - return longs.apply((x, y) -> pair(valueOf(x), valueOf(y))); - } + BigInteger paired = pair(valueOf(x), valueOf(y)); - @Test - public void pairingLongsShouldBeReversible() { - def("Arrays.equals(unpair(pair(longs)), asArray(longs))") - .forAll(DataProvider.LONG_PAIRS) - .suchThat(longs -> Arrays.equals(unpair(makePair(longs)), asArray(longs))) - .check(-1, 100_000) - .assertIsSatisfied(); + assertThat(paired.signum()).isGreaterThan(0); } - private BigInteger[] asArray(Tuple2 longsPair) { - return longsPair.apply((x, y) -> new BigInteger[]{valueOf(x), valueOf(y)}); + @RepeatedTest(1000) + void pairingLongsShouldBeReversible() { + Random random = new Random(); + long x = random.nextLong(); + long y = random.nextLong(); + + BigInteger paired = pair(valueOf(x), valueOf(y)); + BigInteger[] unpaired = unpair(paired); + + assertThat(unpaired).containsExactly(valueOf(x), valueOf(y)); } } \ No newline at end of file diff --git a/friendly-id/src/test/java/com/devskiller/friendly_id/DataProvider.java b/friendly-id/src/test/java/com/devskiller/friendly_id/DataProvider.java deleted file mode 100644 index fb9ec31..0000000 --- a/friendly-id/src/test/java/com/devskiller/friendly_id/DataProvider.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.devskiller.friendly_id; - -import java.math.BigInteger; -import java.util.Random; -import java.util.UUID; - -import io.vavr.Tuple; -import io.vavr.Tuple2; -import io.vavr.test.Arbitrary; -import io.vavr.test.Gen; -import org.assertj.core.util.Strings; - -public class DataProvider { - - public static Arbitrary> LONG_PAIRS = ignored -> { - Gen longs = Gen.choose(Long.MIN_VALUE, Long.MAX_VALUE); - return random -> Tuple.of(longs.apply(random), longs.apply(random)); - }; - static Arbitrary UUIDS = ignored -> random -> UUID.randomUUID(); - static Arbitrary POSITIVE_BIG_INTEGERS = ignored -> random -> - new BigInteger(128, new Random()); - static Arbitrary FRIENDLY_IDS = Arbitrary.string( - Gen.frequency( - Tuple.of(1, Gen.choose('A', 'Z')), - Tuple.of(1, Gen.choose('a', 'z')), - Tuple.of(1, Gen.choose('0', '9')))) - .filter(code -> !Strings.isNullOrEmpty(code)) - .filter(code -> Base62.decode(code, -1).bitLength() <= 128); - -} - diff --git a/friendly-id/src/test/java/com/devskiller/friendly_id/FriendlyIdTest.java b/friendly-id/src/test/java/com/devskiller/friendly_id/FriendlyIdTest.java index 6c8e9c3..1bb19ce 100644 --- a/friendly-id/src/test/java/com/devskiller/friendly_id/FriendlyIdTest.java +++ b/friendly-id/src/test/java/com/devskiller/friendly_id/FriendlyIdTest.java @@ -1,41 +1,43 @@ package com.devskiller.friendly_id; -import io.vavr.test.Arbitrary; -import org.junit.Test; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.Random; +import java.util.UUID; import static com.devskiller.friendly_id.FriendlyId.toFriendlyId; import static com.devskiller.friendly_id.FriendlyId.toUuid; import static com.devskiller.friendly_id.IdUtil.areEqualIgnoringLeadingZeros; -import static io.vavr.test.Property.def; -import static org.assertj.core.util.Objects.areEqual; - -public class FriendlyIdTest { - - @Test - public void shouldCreateValidIdsThatConformToUuidType4() { - def("areEqual(FriendlyId.toUuid(FriendlyId.toFriendlyId(uuid))), uuid)") - .forAll(Arbitrary.integer()) - .suchThat(ignored -> toUuid(FriendlyId.createFriendlyId()).version() == 4) - .check(-1, 100_000) - .assertIsSatisfied(); +import static org.assertj.core.api.Assertions.assertThat; + +class FriendlyIdTest { + + @RepeatedTest(1000) + void shouldCreateValidIdsThatConformToUuidType4() { + UUID uuid = toUuid(FriendlyId.createFriendlyId()); + assertThat(uuid.version()).isEqualTo(4); + } + + @RepeatedTest(1000) + void encodingUuidShouldBeReversible() { + UUID uuid = UUID.randomUUID(); + UUID result = toUuid(toFriendlyId(uuid)); + assertThat(result).isEqualTo(uuid); } - @Test - public void encodingUuidShouldBeReversible() { - def("areEqual(FriendlyId.toUuid(FriendlyId.toFriendlyId(uuid))), uuid)") - .forAll(DataProvider.UUIDS) - .suchThat(uuid -> areEqual(toUuid(toFriendlyId(uuid)), uuid)) - .check(-1, 100_000) - .assertIsSatisfied(); + @RepeatedTest(1000) + void decodingIdShouldBeReversible() { + String id = generateRandomFriendlyId(); + String result = toFriendlyId(toUuid(id)); + assertThat(areEqualIgnoringLeadingZeros(result, id)).isTrue(); } - @Test - public void decodingIdShouldBeReversible() { - def("areEqualIgnoringLeadingZeros(Url62.toFriendlyId(Url62.toUuid(id)), id)") - .forAll(DataProvider.FRIENDLY_IDS) - .suchThat(id -> areEqualIgnoringLeadingZeros(toFriendlyId(toUuid(id)), id)) - .check(100, 100_000) - .assertIsSatisfied(); + private String generateRandomFriendlyId() { + Random random = new Random(); + BigInteger bigInt = new BigInteger(128, random); + return Base62.encode(bigInt); } } \ No newline at end of file diff --git a/friendly-id/src/test/java/com/devskiller/friendly_id/Url62Test.java b/friendly-id/src/test/java/com/devskiller/friendly_id/Url62Test.java index e3d0097..90c9740 100644 --- a/friendly-id/src/test/java/com/devskiller/friendly_id/Url62Test.java +++ b/friendly-id/src/test/java/com/devskiller/friendly_id/Url62Test.java @@ -1,34 +1,34 @@ package com.devskiller.friendly_id; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class Url62Test { +class Url62Test { @Test - public void shouldExplodeWhenContainsIllegalCharacters() { + void shouldExplodeWhenContainsIllegalCharacters() { assertThatThrownBy(() -> Url62.decode("Foo Bar")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("contains illegal characters"); } @Test - public void shouldFaildOnEmptyString() { + void shouldFaildOnEmptyString() { assertThatThrownBy(() -> Url62.decode("")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("must not be empty"); } @Test - public void shouldFailsOnNullString() { + void shouldFailsOnNullString() { assertThatThrownBy(() -> Url62.decode(null)) .isInstanceOf(NullPointerException.class) .hasMessageContaining("must not be null"); } @Test - public void shouldFailsWhenStringContainsMoreThan128bitInformation() { + void shouldFailsWhenStringContainsMoreThan128bitInformation() { assertThatThrownBy(() -> Url62.decode("7NLCAyd6sKR7kDHxgAWFPas")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("contains more than 128bit information"); diff --git a/friendly-id/src/test/java/com/devskiller/friendly_id/type/FriendlyIdTest.java b/friendly-id/src/test/java/com/devskiller/friendly_id/type/FriendlyIdTest.java new file mode 100644 index 0000000..780f31d --- /dev/null +++ b/friendly-id/src/test/java/com/devskiller/friendly_id/type/FriendlyIdTest.java @@ -0,0 +1,137 @@ +package com.devskiller.friendly_id.type; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class FriendlyIdTest { + + @Test + void shouldCreateFromUuid() { + // given + UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); + + // when + FriendlyId friendlyId = FriendlyId.of(uuid); + + // then + assertEquals(uuid, friendlyId.uuid()); + } + + @Test + void shouldCreateFromString() { + // given + String friendlyIdString = "5wbwf6yUxVBcr48AMbz9cb"; + + // when + FriendlyId friendlyId = FriendlyId.fromString(friendlyIdString); + + // then + assertEquals(friendlyIdString, friendlyId.toString()); + } + + @Test + void shouldCreateRandom() { + // when + FriendlyId friendlyId1 = FriendlyId.random(); + FriendlyId friendlyId2 = FriendlyId.random(); + + // then + assertNotEquals(friendlyId1, friendlyId2); + assertNotNull(friendlyId1.uuid()); + assertNotNull(friendlyId2.uuid()); + } + + @Test + void shouldConvertToString() { + // given + UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); + FriendlyId friendlyId = FriendlyId.of(uuid); + + // when + String result = friendlyId.toString(); + + // then + assertEquals(com.devskiller.friendly_id.FriendlyId.toFriendlyId(uuid), result); + } + + @Test + void shouldBeEqualWhenUuidIsEqual() { + // given + UUID uuid = UUID.randomUUID(); + FriendlyId id1 = FriendlyId.of(uuid); + FriendlyId id2 = FriendlyId.of(uuid); + + // then + assertEquals(id1, id2); + assertEquals(id1.hashCode(), id2.hashCode()); + } + + @Test + void shouldNotBeEqualWhenUuidIsDifferent() { + // given + FriendlyId id1 = FriendlyId.random(); + FriendlyId id2 = FriendlyId.random(); + + // then + assertNotEquals(id1, id2); + } + + @Test + void shouldBeComparable() { + // given + UUID uuid1 = UUID.fromString("00000000-0000-0000-0000-000000000001"); + UUID uuid2 = UUID.fromString("00000000-0000-0000-0000-000000000002"); + FriendlyId id1 = FriendlyId.of(uuid1); + FriendlyId id2 = FriendlyId.of(uuid2); + + // then + assertTrue(id1.compareTo(id2) < 0); + assertTrue(id2.compareTo(id1) > 0); + assertEquals(0, id1.compareTo(id1)); + } + + @Test + void shouldThrowExceptionWhenUuidIsNull() { + // when/then + assertThrows(NullPointerException.class, () -> FriendlyId.of(null)); + } + + @Test + void shouldThrowExceptionWhenStringIsNull() { + // when/then + assertThrows(NullPointerException.class, () -> FriendlyId.fromString(null)); + } + + @Test + void shouldBeReversible() { + // given + UUID originalUuid = UUID.randomUUID(); + FriendlyId friendlyId = FriendlyId.of(originalUuid); + + // when + String friendlyIdString = friendlyId.toString(); + FriendlyId reconstructed = FriendlyId.fromString(friendlyIdString); + + // then + assertEquals(friendlyId, reconstructed); + assertEquals(originalUuid, reconstructed.uuid()); + } + + @Test + void shouldBeEqualRegardlessOfCreationMethod() { + // given + UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); + + // when + FriendlyId fromUuid = FriendlyId.of(uuid); + FriendlyId fromString = FriendlyId.fromString(com.devskiller.friendly_id.FriendlyId.toFriendlyId(uuid)); + + // then + assertEquals(fromUuid, fromString); + assertEquals(fromUuid.hashCode(), fromString.hashCode()); + assertEquals(fromUuid.toString(), fromString.toString()); + } +} diff --git a/mvnw b/mvnw index d2f0ea3..bd8896b 100755 --- a/mvnw +++ b/mvnw @@ -19,292 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -########################################################################################## -# End of extension -########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index b26ab24..5761d94 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,182 +1,189 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index 69edfbb..9c752ba 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.devskiller.friendly-id friendly-id-project pom - 1.1.1-SNAPSHOT + ${revision} friendly id Library to convert uuid to url friendly IDs basing on base62 @@ -15,17 +15,26 @@ friendly-id friendly-id-jackson-datatype + friendly-id-jackson2-datatype + friendly-id-jooq + friendly-id-jpa + friendly-id-openfeign friendly-id-spring-boot friendly-id-spring-boot-starter friendly-id-samples + + 2.0.0-SNAPSHOT + UTF-8 UTF-8 - 1.8 - 1.8 - 2.2.2.RELEASE + 21 + 21 + 4.0.1 + 2025.0.0 + 3.20.1 @@ -39,7 +48,7 @@ - Mariusz Smykula + Mariusz S mariuszs@gmail.com Devskiller @@ -53,16 +62,16 @@ - - oss - Sonatype Nexus Snapshots - http://oss.sonatype.org/content/repositories/snapshots - - oss - Nexus Release Repository - http://oss.sonatype.org/service/local/staging/deploy/maven2/ + central + Central Repository + https://central.sonatype.com/api/v1/publisher + + central + Central Repository Snapshots + https://central.sonatype.com/api/v1/publisher + @@ -71,8 +80,8 @@ - Travis - https://travis-ci.org/Devskiller/friendly-id + GitHub Actions + https://github.com/SkillPanel/friendly-id/actions @@ -84,17 +93,28 @@ pom import
+ + org.jooq + jooq + ${jooq.version} + + + jakarta.persistence + jakarta.persistence-api + 3.1.0 + org.assertj assertj-core - 3.12.0 + 3.27.3 test - io.vavr - vavr-test - 0.10.0 - test + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import
@@ -105,20 +125,20 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 maven-surefire-plugin - 2.22.2 + 3.5.4 maven-install-plugin - 2.5.2 + 3.1.3 org.apache.maven.plugins maven-release-plugin - 2.5.3 + 3.1.1 true false @@ -130,7 +150,7 @@ org.codehaus.mojo flatten-maven-plugin - 1.1.0 + 1.6.0 ${project.build.directory} true @@ -156,13 +176,18 @@ org.jacoco jacoco-maven-plugin - 0.8.5 + 0.8.12 org.eluder.coveralls coveralls-maven-plugin 4.3.0 + + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + @@ -198,7 +223,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + 3.2.8 sign-artifacts @@ -212,7 +237,7 @@ org.apache.maven.plugins maven-source-plugin - 3.2.1 + 3.3.1 attach-sources @@ -225,7 +250,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.1.1 + 3.10.1 attach-javadocs @@ -236,14 +261,14 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 true - sonatype-nexus-staging - https://oss.sonatype.org/ - true + central + true + published