From fbf8c1c6dcd1236af4a7c12a5e5c478f9ad74786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Smyku=C5=82a?= Date: Fri, 3 Oct 2025 19:26:30 +0200 Subject: [PATCH 01/27] refactor: modernize Spring Boot samples and starter for 3.x best practices - Migrate domain classes to Java 21 records (Foo, Bar) - Replace Lombok @Value with @Data in HATEOAS resources to avoid final classes - Convert assemblers to @Component with dependency injection - Remove manual JsonConfiguration in favor of component scanning - Replace manual toFriendlyId() calls with methodOn() for automatic conversion - Add @Import annotations to @WebMvcTest for assembler beans - Replace @Slf4j for cleaner logging Spring Boot Starter improvements: - Use @AutoConfiguration instead of @Configuration - Replace @ConditionalOnExpression with @ConditionalOnProperty - Add @ConditionalOnWebApplication for better targeting - Remove deprecated spring.factories in favor of AutoConfiguration.imports - Add comprehensive JavaDoc documentation - Document public converter classes Sample projects updates: - JUnit 4 to JUnit 5 migration in contracts sample - Update Spring Cloud Contract to 4.1.5 with JUnit 5 support - Fix content negotiation in customized sample - Modernize HATEOAS link building patterns - Remove redundant .sdkmanrc files from subdirectories All 13 tests passing across 4 sample projects --- .mvn/wrapper/MavenWrapperDownloader.java | 117 ----- .mvn/wrapper/maven-wrapper.jar | Bin 50710 -> 0 bytes .mvn/wrapper/maven-wrapper.properties | 5 +- .sdkmanrc | 5 + CLAUDE.md | 163 ++++++ .../friendly-id-contracts/pom.xml | 10 +- .../sample/contracts/BarResource.java | 12 +- .../contracts/BarResourceAssembler.java | 29 +- .../sample/contracts/FooController.java | 25 +- .../sample/contracts/FooResource.java | 14 +- .../contracts/FooResourceAssembler.java | 17 +- .../sample/contracts/JsonConfiguration.java | 22 - .../sample/contracts/domain/Bar.java | 13 +- .../sample/contracts/domain/Foo.java | 11 +- .../sample/contracts/BarControllerTest.java | 7 +- .../contracts/ContractVerifierBase.java | 9 +- .../sample/contracts/FooControllerTest.java | 9 +- .../friendly_id/sample/contracts/MvcTest.java | 4 +- .../pom.xml | 15 +- .../friendly_id/sample/customized/Bar.java | 15 +- .../sample/customized/BarController.java | 7 +- .../sample/customized/FooService.java | 13 +- .../sample/customized/ApplicationTest.java | 30 +- .../friendly-id-spring-boot-hateos/pom.xml | 11 +- .../sample/hateos/BarResource.java | 12 +- .../sample/hateos/BarResourceAssembler.java | 18 +- .../sample/hateos/FooController.java | 25 +- .../sample/hateos/FooResource.java | 30 +- .../sample/hateos/FooResourceAssembler.java | 27 +- .../sample/hateos/JsonConfiguration.java | 22 - .../friendly_id/sample/hateos/domain/Bar.java | 13 +- .../friendly_id/sample/hateos/domain/Foo.java | 11 +- .../sample/hateos/BarControllerTest.java | 15 +- .../sample/hateos/FooControllerTest.java | 23 +- .../friendly-id-spring-boot-simple/pom.xml | 17 +- .../friendly_id/sample/simple/Bar.java | 7 + .../sample/simple/ApplicationTest.java | 21 +- .../boot/FriendlyIdAutoConfiguration.java | 21 +- .../main/resources/META-INF/spring.factories | 1 - .../friendly_id/spring/EnableFriendlyId.java | 26 + .../spring/FriendlyIdConfiguration.java | 28 +- friendly-id/pom.xml | 9 +- .../com/devskiller/friendly_id/Base62.java | 6 +- .../friendly_id/AnalyzeGeneratedIdsTest.java | 8 +- .../devskiller/friendly_id/Base62Test.java | 45 +- .../friendly_id/BigIntegerPairingTest.java | 50 +- .../devskiller/friendly_id/DataProvider.java | 31 -- .../friendly_id/FriendlyIdTest.java | 58 ++- .../com/devskiller/friendly_id/Url62Test.java | 12 +- mvnw | 477 +++++++++--------- mvnw.cmd | 371 +++++++------- pom.xml | 36 +- 52 files changed, 996 insertions(+), 987 deletions(-) delete mode 100644 .mvn/wrapper/MavenWrapperDownloader.java delete mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .sdkmanrc create mode 100644 CLAUDE.md delete mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/JsonConfiguration.java delete mode 100644 friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/JsonConfiguration.java delete mode 100644 friendly-id-spring-boot-starter/src/main/resources/META-INF/spring.factories delete mode 100644 friendly-id/src/test/java/com/devskiller/friendly_id/DataProvider.java 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 0d5e649888a4843c1520054d9672f80c62ebbb48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50710 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�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 JS$+5G{{hr`vzY(@ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 15fbe3d..c0bcafe 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.11/apache-maven-3.9.11-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/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1cade99 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,163 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +FriendlyID is a Java library that converts UUIDs (36 characters) to URL-friendly Base62-encoded IDs (max 22 characters). Example: `c3587ec5-0976-497f-8374-61e0c2ea3da5` → `5wbwf6yUxVBcr48AMbz9cb` + +**Technology Stack:** +- Java 21 +- Spring Boot 3.4.1 +- JUnit 5 (Jupiter) +- Maven multi-module project + +## Building and Testing + +### Build Commands + +```bash +# Build entire project (skipping tests) +mvn clean install -DskipTests + +# Build only core modules (most common during development) +mvn clean install -DskipTests -pl friendly-id,friendly-id-jackson-datatype,friendly-id-spring-boot,friendly-id-spring-boot-starter -am + +# Full build with tests +mvn clean install + +# Build specific module +mvn clean install -pl friendly-id + +# Run tests for specific module +mvn test -pl friendly-id + +# Run single test class +mvn test -pl friendly-id -Dtest=FriendlyIdTest + +# Run single test method +mvn test -pl friendly-id -Dtest=FriendlyIdTest#shouldCreateValidIdsThatConformToUuidType4 +``` + +### Test Execution Notes + +- Core library tests use `@RepeatedTest` extensively (1000 iterations) to ensure robustness +- Sample projects require Spring Boot context startup, which takes ~1-2 seconds +- Tests are designed to be fast: full test suite runs in under 10 seconds + +## Architecture + +### Module Structure + +**Multi-module Maven project with 5 modules:** + +1. **`friendly-id`** (core library) + - Pure Java, no dependencies + - Main classes: `FriendlyId`, `Base62`, `Url62`, `UuidConverter` + - Conversion logic between UUID ↔ Base62 string + +2. **`friendly-id-jackson-datatype`** + - Jackson integration for JSON serialization/deserialization + - `FriendlyIdModule` - registers custom serializers for UUID fields + - Automatically converts UUID to FriendlyID in JSON output + +3. **`friendly-id-spring-boot`** + - Spring MVC integration + - `FriendlyIdConfiguration` - registers Spring converters + - Enables `@PathVariable UUID` to accept FriendlyID strings in URLs + - Implements `WebMvcConfigurer` to add formatters + +4. **`friendly-id-spring-boot-starter`** + - Auto-configuration for Spring Boot + - Uses `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` + - Just add to dependencies - no manual configuration needed + +5. **`friendly-id-samples`** + - Four example Spring Boot applications + - Demonstrates different integration scenarios + +### Key Design Patterns + +**UUID Conversion Flow:** +``` +UUID (128-bit) → BigInteger → Base62 String → FriendlyID +``` + +**Spring Integration Pattern:** +- Uses Spring's `Converter` interface for bidirectional conversion +- `StringToUuidConverter`: FriendlyID string → UUID (for @PathVariable) +- `UuidToStringConverter`: UUID → FriendlyID string (for JSON serialization) +- Jackson module handles JSON serialization separately + +**Auto-configuration:** +- Spring Boot 3 uses `AutoConfiguration.imports` instead of `spring.factories` +- Conditional on class presence: `@ConditionalOnExpression` with property flag +- Default enabled, can be disabled with: `com.devskiller.friendly_id.auto=false` + +### Important Implementation Details + +**Base62 Encoding:** +- Character set: `[0-9A-Za-z]` (62 characters) +- Leading zeros are ignored during decoding +- Maximum 22 characters for full 128-bit UUID +- Shorter UUIDs produce shorter FriendlyIDs + +**Spring Boot 3 Requirements:** +- Compiler must use `-parameters` flag for parameter name reflection +- All sample projects have `true` in maven-compiler-plugin +- Without this flag, `@PathVariable` will fail with parameter name error + +**Testing Strategy:** +- Property-based testing replaced with `@RepeatedTest(1000)` +- Random data generation using `BigInteger(128, new Random())` +- Reversibility tests: encode→decode→encode should equal original + +## Common Development Scenarios + +### Adding New Integration + +When adding support for a new framework (e.g., Micronaut, Quarkus): + +1. Create new module: `friendly-id-{framework}-integration` +2. Add converter/formatter registration for the framework +3. Register Jackson module if framework uses Jackson +4. Create sample project in `friendly-id-samples/` + +### Modifying Base62 Algorithm + +Core conversion logic is in `friendly-id/src/main/java/com/devskiller/friendly_id/Base62.java`: +- `encode(BigInteger)` - converts number to Base62 string +- `decode(String)` - converts Base62 string back to number +- Validate changes don't break 128-bit limit +- Ensure reversibility: all existing tests must pass + +### Testing Spring Boot Integration + +Sample projects demonstrate real-world usage: +- `friendly-id-spring-boot-simple` - basic REST controller +- `friendly-id-spring-boot-customized` - custom configuration +- `friendly-id-spring-boot-hateos` - HATEOAS integration +- `friendly-id-contracts` - Spring Cloud Contract testing + +## Project-Specific Conventions + +- Use conventional commits format (https://www.conventionalcommits.org/) +- All code comments must be in English +- Tests use JUnit 5 (not JUnit 4) - no `@RunWith`, use plain `@Test` +- Prefer `@RepeatedTest` over property-based testing libraries +- Maven compiler version: 3.13.0+ (for Java 21 support) + +## Migration Notes (Current State) + +This project has recently been upgraded: +- Java 8 → 21 +- Spring Boot 2.2.2 → 3.4.1 +- JUnit 4 → JUnit 5 +- Vavr property testing → JUnit 5 `@RepeatedTest` +- GitHub Actions workflow still references Java 1.8 (needs update) + +## Known Issues + +- FIXME in `FriendlyIdConfiguration`: `StringToUuidConverter` should be public for better extensibility +- Sample `friendly-id-spring-boot-hateos` may need Lombok annotation processor configuration +- Travis CI badge in README is outdated (project uses GitHub Actions) diff --git a/friendly-id-samples/friendly-id-contracts/pom.xml b/friendly-id-samples/friendly-id-contracts/pom.xml index 8c47473..d599549 100644 --- a/friendly-id-samples/friendly-id-contracts/pom.xml +++ b/friendly-id-samples/friendly-id-contracts/pom.xml @@ -10,15 +10,15 @@ org.springframework.boot spring-boot-starter-parent - 2.2.2.RELEASE + 3.4.1 UTF-8 UTF-8 - 1.8 - 2.2.1.RELEASE + 21 + 4.1.5 @@ -91,7 +91,7 @@ maven-compiler-plugin - 3.8.0 + 3.14.0 true @@ -106,7 +106,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.1 org.springframework.cloud @@ -116,6 +115,7 @@ com.devskiller.friendly_id.sample.contracts com.devskiller.friendly_id.sample.contracts.ContractVerifierBase + JUNIT5 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 index a7032c2..805f415 100644 --- 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 @@ -1,13 +1,19 @@ package com.devskiller.friendly_id.sample.contracts; -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-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 index 8f672a7..66b8e93 100644 --- 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 @@ -1,32 +1,29 @@ 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; +import org.springframework.stereotype.Component; -public class BarResourceAssembler extends RepresentationModelAssemblerSupport { +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; - LinkRelationProvider relProvider; +@Component +public class BarResourceAssembler extends RepresentationModelAssemblerSupport { 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()); + 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-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 index c8065cc..1b3eb0b 100644 --- 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 @@ -1,28 +1,26 @@ 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 lombok.extern.slf4j.Slf4j; 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.net.URI; import java.util.UUID; +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 EntityLinks entityLinks; private final FooResourceAssembler assembler; @@ -47,13 +45,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()); - // ... + // Modern Spring HATEOAS 2.x - methodOn() triggers automatic FriendlyId conversion + URI location = linkTo(methodOn(FooController.class) + .get(fooResource.getUuid())) + .toUri(); - headers.setLocation(entityLinks.linkToItemResource(FooResource.class, FriendlyId.toFriendlyId(entity.getId())).toUri()); - return new ResponseEntity<>(headers, HttpStatus.CREATED); + return ResponseEntity.created(location).build(); } } 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 index 6a33beb..4746394 100644 --- 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 @@ -1,16 +1,22 @@ package com.devskiller.friendly_id.sample.contracts; -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; import java.util.UUID; @Relation(value = "foos") -@Value +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor public class FooResource extends RepresentationModel { - private final UUID uuid; - private final String name; + UUID uuid; + 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 index 3b7e367..26e0c27 100644 --- 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 @@ -1,10 +1,13 @@ 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; +import org.springframework.stereotype.Component; +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() { @@ -13,9 +16,13 @@ public FooResourceAssembler() { @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()); + FooResource resource = new FooResource(entity.id(), entity.name()); + + // Modern Spring HATEOAS 2.x - methodOn() triggers automatic FriendlyId conversion + resource.add(linkTo(methodOn(FooController.class) + .get(entity.id())) + .withSelfRel()); + return resource; } } 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/domain/Bar.java b/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/domain/Bar.java index 3844548..78880ac 100644 --- 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 @@ -1,17 +1,6 @@ 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; - +public record Bar(UUID id, String name, 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 index 343fe77..dcff90f 100644 --- 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 @@ -1,15 +1,6 @@ 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; - +public record Foo(UUID id, String name) { } 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 index 3000a00..b375c2b 100644 --- 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 @@ -1,11 +1,10 @@ package com.devskiller.friendly_id.sample.contracts; -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.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; import com.devskiller.friendly_id.spring.EnableFriendlyId; @@ -17,9 +16,9 @@ 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 +@Import(BarResourceAssembler.class) public class BarControllerTest { @Autowired 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..dba975a 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,25 +1,24 @@ 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.context.annotation.Import; import org.springframework.web.context.WebApplicationContext; import com.devskiller.friendly_id.spring.EnableFriendlyId; -@RunWith(SpringRunner.class) @WebMvcTest @EnableFriendlyId +@Import({FooResourceAssembler.class, BarResourceAssembler.class}) public abstract class ContractVerifierBase { @Autowired private WebApplicationContext context; - @Before + @BeforeEach public void setUp() { RestAssuredMockMvc.webAppContextSetup(context); } 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 index fae95d7..1719e72 100644 --- 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 @@ -1,11 +1,10 @@ package com.devskiller.friendly_id.sample.contracts; -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.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; import com.devskiller.friendly_id.spring.EnableFriendlyId; @@ -20,9 +19,9 @@ 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 +@Import(FooResourceAssembler.class) public class FooControllerTest { @Autowired @@ -40,7 +39,7 @@ public void shouldGet() throws Exception { @Test public void shouldCreate() throws Exception { - mockMvc.perform(post("/foos/") + mockMvc.perform(post("/foos") .content("{\"uuid\":\"newFoo\",\"name\":\"Very New Foo\"}") .contentType("application/hal+json")) .andDo(print()) 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 index 2c5f697..13256e1 100644 --- 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 @@ -7,7 +7,7 @@ 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.junit.jupiter.api.BeforeEach; import org.springframework.core.convert.converter.Converter; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.hateoas.server.EntityLinks; @@ -24,7 +24,7 @@ public class MvcTest { protected StandaloneMockMvcBuilder mockMvcBuilder; - @Before + @BeforeEach public void setup() { mockMvcBuilder = standaloneSetup(new FooController(mock(EntityLinks.class))); DefaultFormattingConversionService service = new DefaultFormattingConversionService(); 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..c361857 100644 --- a/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml @@ -10,14 +10,14 @@ org.springframework.boot spring-boot-starter-parent - 2.2.2.RELEASE + 3.4.1 UTF-8 UTF-8 - 1.8 + 21 @@ -57,22 +57,21 @@ maven-compiler-plugin - 3.8.1 + 3.14.0 true 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 index 2de1b20..7b5fd41 100644 --- 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 @@ -2,22 +2,9 @@ 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; - } +record Bar(UUID friendlyId, @IdFormat(RAW) 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 index d0dfd90..e7a3663 100644 --- 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 @@ -1,10 +1,8 @@ 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 lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -13,12 +11,11 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Slf4j @RestController @RequestMapping("/bars") public class BarController { - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final FooService fooService; public BarController(FooService fooService) { 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 index 521afd3..a2eab23 100644 --- 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 @@ -2,17 +2,20 @@ import java.util.UUID; +import lombok.extern.slf4j.Slf4j; + import org.springframework.stereotype.Service; +@Slf4j @Service -class FooService { +public class FooService { - Bar find(UUID uuid) { - System.out.println("find: " + uuid); + public Bar find(UUID uuid) { + log.info("find: {}",uuid); return new Bar(uuid, uuid); } - void update(UUID id, Bar bar) { - System.out.println("update: " + id + ":" + bar); + public void update(UUID id, Bar bar) { + log.info("update: {}:{}", id, bar); } } 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..ff8ba2e 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 @@ -2,19 +2,19 @@ import com.devskiller.friendly_id.FriendlyId; import com.devskiller.friendly_id.spring.EnableFriendlyId; -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.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; 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 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; @@ -22,26 +22,25 @@ 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) @EnableFriendlyId -public class ApplicationTest { +class ApplicationTest { @Autowired MockMvc mockMvc; - @MockBean + @MockitoBean FooService fooService; @Test - public void shouldSerialize() throws Exception { - + void shouldSerialize() throws Exception { // given UUID uuid = UUID.randomUUID(); given(fooService.find(uuid)).willReturn(new Bar(uuid, uuid)); // expect - mockMvc.perform(get("/bars/{id}", FriendlyId.toFriendlyId(uuid))) + mockMvc.perform(get("/bars/{id}", FriendlyId.toFriendlyId(uuid)) + .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -50,8 +49,7 @@ public void shouldSerialize() throws Exception { } @Test - public void shouldDeserialize() throws Exception { - + void shouldDeserialize() throws Exception { // given UUID uuid = UUID.randomUUID(); String json = "{\"friendlyId\":\"" + FriendlyId.toFriendlyId(uuid) + "\",\"uuid\":\"" + uuid + "\"}"; @@ -65,18 +63,18 @@ public void shouldDeserialize() throws Exception { // then then(fooService) - .should().update(uuid, new Bar(uuid, uuid)); + .should().update(eq(uuid), any(Bar.class)); } @Test - public void sampleTestUsingPseudoUuid() throws Exception { - + void sampleTestUsingPseudoUuid() throws Exception { // given UUID barId = toUuid("barId"); given(fooService.find(barId)).willReturn(new Bar(barId, barId)); // expect - mockMvc.perform(get("/bars/{id}", "barId")) + mockMvc.perform(get("/bars/{id}", "barId") + .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 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..e90ab0f 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml @@ -10,14 +10,14 @@ org.springframework.boot spring-boot-starter-parent - 2.2.2.RELEASE + 3.4.1 UTF-8 UTF-8 - 1.8 + 21 @@ -37,7 +37,7 @@ org.atteo evo-inflector - 1.2.2 + 1.3 com.fasterxml.jackson.module @@ -73,7 +73,10 @@ maven-compiler-plugin - 3.8.0 + 3.14.0 + + true + 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..fbf4a2e 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.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..3d6f677 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.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-simple/pom.xml b/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml index 2df02d2..e7f5552 100644 --- a/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml @@ -10,14 +10,14 @@ org.springframework.boot spring-boot-starter-parent - 2.2.2.RELEASE + 3.4.1 UTF-8 UTF-8 - 1.8 + 21 @@ -46,19 +46,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/Bar.java b/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Bar.java index 7d44099..5b1ecb7 100644 --- 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 @@ -6,6 +6,13 @@ public class Bar { private UUID id; + public Bar() { + } + + public Bar(UUID id) { + this.id = id; + } + public UUID getId() { return id; } 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..12971a5 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 @@ -1,35 +1,30 @@ package com.devskiller.friendly_id.sample.simple; -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 java.util.UUID; import static org.assertj.core.api.BDDAssertions.then; -@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class ApplicationTest { +class ApplicationTest { @Autowired private TestRestTemplate restTemplate; - @Test - public void shouldSerialize() { - + void shouldSerialize() { // given UUID uuid = UUID.randomUUID(); - // expect + // when Bar entity = restTemplate.getForEntity("/bars/{id}", Bar.class, uuid).getBody(); + // then + then(entity).isNotNull(); then(entity.getId()).isEqualTo(uuid); - } } 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/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..59bd4f5 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 @@ -13,6 +13,18 @@ import com.devskiller.friendly_id.FriendlyId; import com.devskiller.friendly_id.jackson.FriendlyIdModule; +/** + * 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 { @@ -27,7 +39,13 @@ public Module friendlyIdModule() { return new FriendlyIdModule(); } - //FIXME: make this public + /** + * Converter that converts FriendlyId strings to UUID. + *

+ * This converter is automatically registered in Spring's conversion service + * and allows path variables and request parameters to be automatically converted + * from FriendlyId format to UUID. + */ public static class StringToUuidConverter implements Converter { @Override @@ -36,7 +54,13 @@ public UUID convert(String id) { } } - + /** + * Converter that converts UUID to FriendlyId strings. + *

+ * This converter is automatically registered in Spring's conversion service + * and allows UUIDs to be automatically converted to FriendlyId format + * in responses and URL generation. + */ public static class UuidToStringConverter implements Converter { @Override diff --git a/friendly-id/pom.xml b/friendly-id/pom.xml index abfd26e..27cd7ed 100644 --- a/friendly-id/pom.xml +++ b/friendly-id/pom.xml @@ -14,8 +14,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter test @@ -23,11 +23,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/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/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..ae3be80 100644 --- a/pom.xml +++ b/pom.xml @@ -23,9 +23,9 @@ UTF-8 UTF-8 - 1.8 - 1.8 - 2.2.2.RELEASE + 21 + 21 + 3.4.1 @@ -39,7 +39,7 @@ - Mariusz Smykula + Mariusz S mariuszs@gmail.com Devskiller @@ -87,13 +87,7 @@ org.assertj assertj-core - 3.12.0 - test - - - io.vavr - vavr-test - 0.10.0 + 3.27.3 test @@ -105,20 +99,20 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.13.0 maven-surefire-plugin - 2.22.2 + 3.5.2 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 +124,7 @@ org.codehaus.mojo flatten-maven-plugin - 1.1.0 + 1.6.0 ${project.build.directory} true @@ -156,7 +150,7 @@ org.jacoco jacoco-maven-plugin - 0.8.5 + 0.8.12 org.eluder.coveralls @@ -198,7 +192,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + 3.2.5 sign-artifacts @@ -212,7 +206,7 @@ org.apache.maven.plugins maven-source-plugin - 3.2.1 + 3.3.1 attach-sources @@ -225,7 +219,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.1.1 + 3.10.1 attach-javadocs @@ -238,7 +232,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.8 + 1.6.13 true sonatype-nexus-staging From 49c861a51a40f2f76a1d32cb32b90c4ec3978fee Mon Sep 17 00:00:00 2001 From: "Mariusz S." Date: Fri, 3 Oct 2025 20:15:23 +0200 Subject: [PATCH 02/27] feat: add jOOQ integration module for FriendlyId - Create new friendly-id-jooq module with jOOQ Converter - Implement bidirectional conversion between UUID and FriendlyId strings - Add comprehensive unit tests with 7 test cases - Document usage with jOOQ code generator in README - Add module to parent POM with jOOQ 3.19.16 dependency management --- friendly-id-jooq/README.md | 78 +++++++++++++ friendly-id-jooq/pom.xml | 36 ++++++ .../friendly_id/jooq/FriendlyIdConverter.java | 106 ++++++++++++++++++ .../friendly_id/jooq/package-info.java | 17 +++ .../jooq/FriendlyIdConverterTest.java | 87 ++++++++++++++ pom.xml | 6 + 6 files changed, 330 insertions(+) create mode 100644 friendly-id-jooq/README.md create mode 100644 friendly-id-jooq/pom.xml create mode 100644 friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/FriendlyIdConverter.java create mode 100644 friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/package-info.java create mode 100644 friendly-id-jooq/src/test/java/com/devskiller/friendly_id/jooq/FriendlyIdConverterTest.java diff --git a/friendly-id-jooq/README.md b/friendly-id-jooq/README.md new file mode 100644 index 0000000..cfb4850 --- /dev/null +++ b/friendly-id-jooq/README.md @@ -0,0 +1,78 @@ +# 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 strings in your Java code while storing UUIDs in the database. jOOQ will automatically handle the conversion between the two representations. + +## 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 + + + + + + java.lang.String + com.devskiller.friendly_id.jooq.FriendlyIdConverter + .*\.ID + UUID + + + + + +``` + +## Example + +```java +// Query using FriendlyId +String friendlyId = "5wbwf6yUxVBcr48AMbz9cb"; +UserRecord user = create + .selectFrom(USER) + .where(USER.ID.eq(friendlyId)) // Automatically converted from FriendlyId to UUID + .fetchOne(); + +// Get FriendlyId from result +String userId = user.getId(); // Returns FriendlyId string, not UUID + +// Insert with FriendlyId +create.insertInto(USER) + .set(USER.ID, FriendlyId.createFriendlyId()) + .set(USER.NAME, "John Doe") + .execute(); +``` + +## How It Works + +The `FriendlyIdConverter` implements `org.jooq.Converter`: + +- **Database Type (fromType)**: `UUID` - the actual column type in your database +- **User Type (toType)**: `String` - the FriendlyId representation in your Java code +- **Conversion**: Bidirectional conversion between UUID and FriendlyId strings + +## Benefits + +- **URL-Friendly**: Use human-readable, Base62-encoded IDs in your API +- **Type Safety**: jOOQ handles conversion automatically +- **Database Efficiency**: Store compact UUIDs in the database +- **Transparent**: No manual conversion needed in your application code + +## 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/) diff --git a/friendly-id-jooq/pom.xml b/friendly-id-jooq/pom.xml new file mode 100644 index 0000000..bf1802d --- /dev/null +++ b/friendly-id-jooq/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + friendly-id-project + com.devskiller.friendly-id + 1.1.1-SNAPSHOT + .. + + + 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..d63b8a5 --- /dev/null +++ b/friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/FriendlyIdConverter.java @@ -0,0 +1,106 @@ +package com.devskiller.friendly_id.jooq; + +import java.util.UUID; + +import org.jooq.Converter; + +import com.devskiller.friendly_id.FriendlyId; + +/** + * jOOQ converter for transparent UUID to FriendlyId conversion in database queries. + *

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

+ * + *

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
+ * 
+ *   
+ *     
+ *       
+ *         
+ *           java.lang.String
+ *           com.devskiller.friendly_id.jooq.FriendlyIdConverter
+ *           .*\.ID
+ *           UUID
+ *         
+ *       
+ *     
+ *   
+ * 
+ * }
+ * + *

Manual Usage Example

+ *
{@code
+ * // Query using FriendlyId
+ * String friendlyId = "cafe";
+ * UserRecord user = create
+ *     .selectFrom(USER)
+ *     .where(USER.ID.eq(friendlyId))
+ *     .fetchOne();
+ *
+ * // Get FriendlyId from result
+ * String userId = user.getId(); // Returns FriendlyId string, not UUID
+ *
+ * // Insert with FriendlyId
+ * create.insertInto(USER)
+ *     .set(USER.ID, "newUserId")
+ *     .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 string. + * + * @param databaseObject the UUID from the database, may be {@code null} + * @return the FriendlyId string representation, or {@code null} if input is {@code null} + */ + @Override + public String from(UUID databaseObject) { + return databaseObject == null ? null : FriendlyId.toFriendlyId(databaseObject); + } + + /** + * Converts a FriendlyId string to a database UUID. + * + * @param userObject the FriendlyId string, may be {@code null} + * @return the UUID representation, or {@code null} if input is {@code null} + */ + @Override + public UUID to(String userObject) { + return userObject == null ? null : FriendlyId.toUuid(userObject); + } + + /** + * Returns the database type (UUID). + * + * @return {@code UUID.class} + */ + @Override + public Class fromType() { + return UUID.class; + } + + /** + * Returns the user type (String). + * + * @return {@code String.class} + */ + @Override + public Class toType() { + return String.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..e6ce5f9 --- /dev/null +++ b/friendly-id-jooq/src/test/java/com/devskiller/friendly_id/jooq/FriendlyIdConverterTest.java @@ -0,0 +1,87 @@ +package com.devskiller.friendly_id.jooq; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.devskiller.friendly_id.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 + String friendlyId = converter.from(uuid); + + // then + assertEquals(FriendlyId.toFriendlyId(uuid), friendlyId); + } + + @Test + void shouldConvertFriendlyIdToUuid() { + // given + String friendlyId = "5wbwf6yUxVBcr48AMbz9cb"; + + // when + UUID uuid = converter.to(friendlyId); + + // then + assertEquals(FriendlyId.toUuid(friendlyId), uuid); + } + + @Test + void shouldHandleNullUuid() { + // when + String 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(String.class, toType); + } + + @Test + void shouldBeReversible() { + // given + UUID originalUuid = UUID.randomUUID(); + + // when + String friendlyId = converter.from(originalUuid); + UUID convertedUuid = converter.to(friendlyId); + + // then + assertEquals(originalUuid, convertedUuid); + } +} diff --git a/pom.xml b/pom.xml index ae3be80..0de3a00 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,7 @@ friendly-id friendly-id-jackson-datatype + friendly-id-jooq friendly-id-spring-boot friendly-id-spring-boot-starter friendly-id-samples @@ -84,6 +85,11 @@ pom import + + org.jooq + jooq + 3.19.16 + org.assertj assertj-core From 9883180b197c36706d4db002d620d23e755e959a Mon Sep 17 00:00:00 2001 From: "Mariusz S." Date: Fri, 3 Oct 2025 20:24:51 +0200 Subject: [PATCH 03/27] refactor: replace String with FriendlyId value object for memory efficiency - Create FriendlyId value object in com.devskiller.friendly_id.type package - Store UUID internally (16 bytes) instead of String (~40-50 bytes) - Compute FriendlyId string representation on demand via toString() - Update jOOQ converter to use Converter instead of Converter - Add comprehensive tests for FriendlyId value object (11 tests) - Update jOOQ converter tests to use FriendlyId type (8 tests) - Document 30-40% memory savings in README Benefits: - Memory efficient: ~28 bytes vs ~40-50 bytes for String - Type safety: Strong typing prevents mixing UUIDs with FriendlyIds - Pretty printing: Automatic FriendlyId string via toString() - Reusable: Can be used with JPA, Jackson, Spring MVC converters --- friendly-id-jooq/README.md | 45 ++++-- .../friendly_id/jooq/FriendlyIdConverter.java | 44 +++--- .../jooq/FriendlyIdConverterTest.java | 31 ++-- .../friendly_id/type/FriendlyId.java | 138 ++++++++++++++++++ .../friendly_id/type/package-info.java | 11 ++ .../friendly_id/type/FriendlyIdTest.java | 122 ++++++++++++++++ 6 files changed, 351 insertions(+), 40 deletions(-) create mode 100644 friendly-id/src/main/java/com/devskiller/friendly_id/type/FriendlyId.java create mode 100644 friendly-id/src/main/java/com/devskiller/friendly_id/type/package-info.java create mode 100644 friendly-id/src/test/java/com/devskiller/friendly_id/type/FriendlyIdTest.java diff --git a/friendly-id-jooq/README.md b/friendly-id-jooq/README.md index cfb4850..f3d90b7 100644 --- a/friendly-id-jooq/README.md +++ b/friendly-id-jooq/README.md @@ -4,7 +4,9 @@ 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 strings in your Java code while storing UUIDs in the database. jOOQ will automatically handle the conversion between the two representations. +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 @@ -26,7 +28,7 @@ Configure the converter in your jOOQ code generation configuration to apply it t - java.lang.String + com.devskiller.friendly_id.type.FriendlyId com.devskiller.friendly_id.jooq.FriendlyIdConverter .*\.ID UUID @@ -40,39 +42,58 @@ Configure the converter in your jOOQ code generation configuration to apply it t ## Example ```java +import com.devskiller.friendly_id.type.FriendlyId; + // Query using FriendlyId -String friendlyId = "5wbwf6yUxVBcr48AMbz9cb"; +FriendlyId friendlyId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); UserRecord user = create .selectFrom(USER) - .where(USER.ID.eq(friendlyId)) // Automatically converted from FriendlyId to UUID + .where(USER.ID.eq(friendlyId)) // Automatically converted to UUID for database .fetchOne(); // Get FriendlyId from result -String userId = user.getId(); // Returns FriendlyId string, not UUID +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.createFriendlyId()) + .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`: +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) | -- **Database Type (fromType)**: `UUID` - the actual column type in your database -- **User Type (toType)**: `String` - the FriendlyId representation in your Java code -- **Conversion**: Bidirectional conversion between UUID and FriendlyId strings +**Result**: ~30-40% memory savings compared to storing FriendlyId as String ## Benefits -- **URL-Friendly**: Use human-readable, Base62-encoded IDs in your API -- **Type Safety**: jOOQ handles conversion automatically +- **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/src/main/java/com/devskiller/friendly_id/jooq/FriendlyIdConverter.java b/friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/FriendlyIdConverter.java index d63b8a5..ad954ac 100644 --- 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 @@ -4,15 +4,20 @@ import org.jooq.Converter; -import com.devskiller.friendly_id.FriendlyId; +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 strings in your Java code while + * 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

*

@@ -25,7 +30,7 @@ * * * - * java.lang.String + * com.devskiller.friendly_id.type.FriendlyId * com.devskiller.friendly_id.jooq.FriendlyIdConverter * .*\.ID * UUID @@ -39,18 +44,19 @@ *

Manual Usage Example

*
{@code
  * // Query using FriendlyId
- * String friendlyId = "cafe";
+ * FriendlyId friendlyId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb");
  * UserRecord user = create
  *     .selectFrom(USER)
  *     .where(USER.ID.eq(friendlyId))
  *     .fetchOne();
  *
  * // Get FriendlyId from result
- * String userId = user.getId(); // Returns FriendlyId string, not UUID
+ * 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, "newUserId")
+ *     .set(USER.ID, FriendlyId.random())
  *     .set(USER.NAME, "John Doe")
  *     .execute();
  * }
@@ -58,30 +64,30 @@ * @see FriendlyId * @see org.jooq.Converter */ -public class FriendlyIdConverter implements Converter { +public class FriendlyIdConverter implements Converter { private static final long serialVersionUID = 1L; /** - * Converts a database UUID to a FriendlyId string. + * Converts a database UUID to a FriendlyId value object. * * @param databaseObject the UUID from the database, may be {@code null} - * @return the FriendlyId string representation, or {@code null} if input is {@code null} + * @return the FriendlyId value object, or {@code null} if input is {@code null} */ @Override - public String from(UUID databaseObject) { - return databaseObject == null ? null : FriendlyId.toFriendlyId(databaseObject); + public FriendlyId from(UUID databaseObject) { + return databaseObject == null ? null : FriendlyId.of(databaseObject); } /** - * Converts a FriendlyId string to a database UUID. + * Converts a FriendlyId value object to a database UUID. * - * @param userObject the FriendlyId string, may be {@code null} + * @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(String userObject) { - return userObject == null ? null : FriendlyId.toUuid(userObject); + public UUID to(FriendlyId userObject) { + return userObject == null ? null : userObject.uuid(); } /** @@ -95,12 +101,12 @@ public Class fromType() { } /** - * Returns the user type (String). + * Returns the user type (FriendlyId). * - * @return {@code String.class} + * @return {@code FriendlyId.class} */ @Override - public Class toType() { - return String.class; + public Class toType() { + return FriendlyId.class; } } 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 index e6ce5f9..d5c533e 100644 --- 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 @@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test; -import com.devskiller.friendly_id.FriendlyId; +import com.devskiller.friendly_id.type.FriendlyId; import static org.junit.jupiter.api.Assertions.*; @@ -18,28 +18,28 @@ void shouldConvertUuidToFriendlyId() { UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); // when - String friendlyId = converter.from(uuid); + FriendlyId friendlyId = converter.from(uuid); // then - assertEquals(FriendlyId.toFriendlyId(uuid), friendlyId); + assertEquals(uuid, friendlyId.uuid()); } @Test void shouldConvertFriendlyIdToUuid() { // given - String friendlyId = "5wbwf6yUxVBcr48AMbz9cb"; + FriendlyId friendlyId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); // when UUID uuid = converter.to(friendlyId); // then - assertEquals(FriendlyId.toUuid(friendlyId), uuid); + assertEquals(friendlyId.uuid(), uuid); } @Test void shouldHandleNullUuid() { // when - String friendlyId = converter.from(null); + FriendlyId friendlyId = converter.from(null); // then assertNull(friendlyId); @@ -66,10 +66,10 @@ void shouldReturnCorrectFromType() { @Test void shouldReturnCorrectToType() { // when - Class toType = converter.toType(); + Class toType = converter.toType(); // then - assertEquals(String.class, toType); + assertEquals(FriendlyId.class, toType); } @Test @@ -78,10 +78,23 @@ void shouldBeReversible() { UUID originalUuid = UUID.randomUUID(); // when - String friendlyId = converter.from(originalUuid); + 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/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..f186a55 --- /dev/null +++ b/friendly-id/src/main/java/com/devskiller/friendly_id/type/FriendlyId.java @@ -0,0 +1,138 @@ +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) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FriendlyId that = (FriendlyId) o; + return 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/type/FriendlyIdTest.java b/friendly-id/src/test/java/com/devskiller/friendly_id/type/FriendlyIdTest.java new file mode 100644 index 0000000..fce725c --- /dev/null +++ b/friendly-id/src/test/java/com/devskiller/friendly_id/type/FriendlyIdTest.java @@ -0,0 +1,122 @@ +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()); + } +} From 9dce4d158ffba79f62ae1b40815ba79eb97935fb Mon Sep 17 00:00:00 2001 From: "Mariusz S." Date: Fri, 3 Oct 2025 20:31:37 +0200 Subject: [PATCH 04/27] feat: add JPA integration module for FriendlyId - Create FriendlyId JPA AttributeConverter with auto-apply - Support Jakarta Persistence API 3.1.0 - Zero-configuration usage in JPA entities - Add comprehensive tests (6 tests) - Document usage with Spring Data JPA in README - Add Jakarta Persistence API to dependency management Benefits: - Auto-apply converter means no @Convert annotation needed - Works seamlessly with Spring Data JPA repositories - Reuses FriendlyId value object from core module - Memory efficient: ~28 bytes vs ~40-50 bytes for String --- friendly-id-jpa/README.md | 180 ++++++++++++++++++ friendly-id-jpa/pom.xml | 37 ++++ .../friendly_id/jpa/FriendlyIdConverter.java | 105 ++++++++++ .../friendly_id/jpa/package-info.java | 15 ++ .../jpa/FriendlyIdConverterTest.java | 82 ++++++++ .../friendly_id/type/FriendlyIdTest.java | 15 ++ pom.xml | 6 + 7 files changed, 440 insertions(+) create mode 100644 friendly-id-jpa/README.md create mode 100644 friendly-id-jpa/pom.xml create mode 100644 friendly-id-jpa/src/main/java/com/devskiller/friendly_id/jpa/FriendlyIdConverter.java create mode 100644 friendly-id-jpa/src/main/java/com/devskiller/friendly_id/jpa/package-info.java create mode 100644 friendly-id-jpa/src/test/java/com/devskiller/friendly_id/jpa/FriendlyIdConverterTest.java 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..12dbd6c --- /dev/null +++ b/friendly-id-jpa/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + + friendly-id-project + com.devskiller.friendly-id + 1.1.1-SNAPSHOT + .. + + + 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/src/test/java/com/devskiller/friendly_id/type/FriendlyIdTest.java b/friendly-id/src/test/java/com/devskiller/friendly_id/type/FriendlyIdTest.java index fce725c..780f31d 100644 --- 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 @@ -119,4 +119,19 @@ void shouldBeReversible() { 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/pom.xml b/pom.xml index 0de3a00..cdc53b6 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ friendly-id friendly-id-jackson-datatype friendly-id-jooq + friendly-id-jpa friendly-id-spring-boot friendly-id-spring-boot-starter friendly-id-samples @@ -90,6 +91,11 @@ jooq 3.19.16
+ + jakarta.persistence + jakarta.persistence-api + 3.1.0 + org.assertj assertj-core From 0b57ad34d807b11207323cb605db1f47a0705060 Mon Sep 17 00:00:00 2001 From: "Mariusz S." Date: Fri, 3 Oct 2025 20:57:12 +0200 Subject: [PATCH 05/27] feat: add OpenFeign integration and standardize Maven configuration - Add friendly-id-openfeign module with automatic FriendlyId encoding/decoding for Feign clients - Add converters for FriendlyId value object in Spring MVC (StringToFriendlyIdConverter, FriendlyIdToStringConverter) - Standardize all pom.xml files: add name/description tags, consolidate dependency management - Migrate friendly-id-jackson-datatype tests from JUnit 4 to JUnit 5 - Move Spring Cloud dependencies to parent pom dependencyManagement - Update README.md with documentation for jOOQ, JPA, and OpenFeign integrations --- .serena/.gitignore | 1 + .serena/project.yml | 67 ++++++++++++++ README.md | 89 ++++++++++++++++++- friendly-id-jackson-datatype/pom.xml | 9 +- .../spring/FieldWithoutFriendlyIdTest.java | 14 +-- .../spring/FriendlyIdDeserializerTest.java | 8 +- friendly-id-openfeign/pom.xml | 41 +++++++++ .../openfeign/FriendlyIdConfiguration.java | 56 ++++++++++++ .../openfeign/FriendlyIdDecoder.java | 53 +++++++++++ .../openfeign/FriendlyIdEncoder.java | 47 ++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../openfeign/FriendlyIdDecoderTest.java | 78 ++++++++++++++++ .../openfeign/FriendlyIdEncoderTest.java | 74 +++++++++++++++ friendly-id-spring-boot-starter/pom.xml | 6 +- friendly-id-spring-boot/pom.xml | 12 +-- .../spring/FriendlyIdConfiguration.java | 32 +++++++ friendly-id/pom.xml | 3 + pom.xml | 9 ++ 18 files changed, 577 insertions(+), 23 deletions(-) create mode 100644 .serena/.gitignore create mode 100644 .serena/project.yml create mode 100644 friendly-id-openfeign/pom.xml create mode 100644 friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdConfiguration.java create mode 100644 friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdDecoder.java create mode 100644 friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdEncoder.java create mode 100644 friendly-id-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 friendly-id-openfeign/src/test/java/com/devskiller/friendly_id/openfeign/FriendlyIdDecoderTest.java create mode 100644 friendly-id-openfeign/src/test/java/com/devskiller/friendly_id/openfeign/FriendlyIdEncoderTest.java diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..14d86ad --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..64cbcbf --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,67 @@ +# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) +# * For C, use cpp +# * For JavaScript, use typescript +# Special requirements: +# * csharp: Requires the presence of a .sln file in the project folder. +language: java + +# whether to use the project's gitignore file to ignore files +# Added on 2025-04-07 +ignore_all_files_in_gitignore: true +# list of additional paths to ignore +# same syntax as gitignore, so you can use * and ** +# Was previously called `ignored_dirs`, please update your config if you are using that. +# Added (renamed) on 2025-04-07 +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +project_name: "friendly-id" diff --git a/README.md b/README.md index e3489da..8eed306 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 + 1.1.0 + +``` + +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 + 1.1.0 + +``` + +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 + 1.1.0 + +``` + +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/friendly-id-jackson-datatype/pom.xml b/friendly-id-jackson-datatype/pom.xml index fe2ed3d..598e61c 100644 --- a/friendly-id-jackson-datatype/pom.xml +++ b/friendly-id-jackson-datatype/pom.xml @@ -11,6 +11,9 @@ friendly-id-jackson-datatype + FriendlyId Jackson Datatype + Jackson module for JSON serialization/deserialization of UUIDs as FriendlyIds + com.devskiller.friendly-id @@ -35,8 +38,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter test @@ -49,8 +52,8 @@ + org.apache.maven.plugins maven-compiler-plugin - 3.8.1 true 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..aa99b6d 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 @@ -4,20 +4,20 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; -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 FieldWithoutFriendlyIdTest { +class FieldWithoutFriendlyIdTest { - private UUID uuid = UUID.fromString("f088ce5b-9279-4cc3-946a-c15ad740dd6d"); + private final UUID uuid = UUID.fromString("f088ce5b-9279-4cc3-946a-c15ad740dd6d"); private ObjectMapper mapper = mapper(); @Test - public void shouldAllowToDoNotCodeUuidInDataObject() throws Exception { + void shouldAllowToDoNotCodeUuidInDataObject() throws Exception { Foo foo = new Foo(); foo.setRawUuid(uuid); foo.setFriendlyId(uuid); @@ -33,7 +33,7 @@ public void shouldAllowToDoNotCodeUuidInDataObject() throws Exception { } @Test - public void shouldDeserializeUuidsInDataObject() throws Exception { + void shouldDeserializeUuidsInDataObject() throws Exception { String json = "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}"; Foo cloned = mapper.readValue(json, Foo.class); @@ -43,7 +43,7 @@ public void shouldDeserializeUuidsInDataObject() throws Exception { @Test - public void shouldSerializeUuidsInValueObject() throws Exception { + void shouldSerializeUuidsInValueObject() throws Exception { mapper = mapper(new ParameterNamesModule()); Bar bar = new Bar(uuid, uuid); @@ -56,7 +56,7 @@ public void shouldSerializeUuidsInValueObject() throws Exception { } @Test - public void shouldDeserializeUuuidsValueObject() throws Exception { + void shouldDeserializeUuuidsValueObject() throws Exception { mapper = mapper(new ParameterNamesModule()); String json = "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}"; 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..e0525b3 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() throws Exception { 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() throws Exception { String friendlyId = "2YSfgVHnEYbYgfFKhEX3Sz"; UUID uuid = mapper().readValue("\"" + friendlyId + "\"", UUID.class); assertThat(uuid).isEqualByComparingTo(FriendlyId.toUuid(friendlyId)); diff --git a/friendly-id-openfeign/pom.xml b/friendly-id-openfeign/pom.xml new file mode 100644 index 0000000..b7a5824 --- /dev/null +++ b/friendly-id-openfeign/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + friendly-id-project + com.devskiller.friendly-id + 1.1.1-SNAPSHOT + .. + + + 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} + + + org.springframework.cloud + spring-cloud-starter-openfeign + 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..2942549 --- /dev/null +++ b/friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdConfiguration.java @@ -0,0 +1,56 @@ +package com.devskiller.friendly_id.openfeign; + +import java.util.UUID; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import feign.codec.Decoder; +import feign.codec.Encoder; + +/** + * Auto-configuration for FriendlyId integration with Spring Cloud OpenFeign. + *

+ * This configuration automatically registers custom encoder and decoder that handle + * FriendlyId conversion in Feign clients: + *

+ *
    + *
  • Encoder: Converts UUID/FriendlyId parameters to FriendlyId strings in requests
  • + *
  • Decoder: Converts FriendlyId strings to UUID/FriendlyId in responses
  • + *
+ * + *

Usage Example

+ *
{@code
+ * @FeignClient(name = "user-service")
+ * public interface UserClient {
+ *
+ *     @GetMapping("/users/{id}")
+ *     UserDto getUser(@PathVariable UUID id);
+ *
+ *     @GetMapping("/users/{id}/profile")
+ *     ProfileDto getProfile(@PathVariable com.devskiller.friendly_id.type.FriendlyId id);
+ * }
+ *
+ * // Usage
+ * UUID userId = UUID.randomUUID();
+ * UserDto user = userClient.getUser(userId); // Sends as FriendlyId string
+ * }
+ * + * @since 1.1.1 + */ +@Configuration +@ConditionalOnClass(FeignClient.class) +public class FriendlyIdConfiguration { + + @Bean + public Encoder friendlyIdEncoder(Encoder defaultEncoder) { + return new FriendlyIdEncoder(defaultEncoder); + } + + @Bean + public Decoder friendlyIdDecoder(Decoder defaultDecoder) { + return new FriendlyIdDecoder(defaultDecoder); + } +} 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/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/friendly-id-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..308d1cf --- /dev/null +++ b/friendly-id-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.devskiller.friendly_id.openfeign.FriendlyIdConfiguration 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-spring-boot-starter/pom.xml b/friendly-id-spring-boot-starter/pom.xml index 4323276..6612b0d 100644 --- a/friendly-id-spring-boot-starter/pom.xml +++ b/friendly-id-spring-boot-starter/pom.xml @@ -1,15 +1,19 @@ + 4.0.0 + com.devskiller.friendly-id friendly-id-project 1.1.1-SNAPSHOT .. - 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 diff --git a/friendly-id-spring-boot/pom.xml b/friendly-id-spring-boot/pom.xml index 8b8f357..cb74800 100644 --- a/friendly-id-spring-boot/pom.xml +++ b/friendly-id-spring-boot/pom.xml @@ -1,24 +1,24 @@ + 4.0.0 + com.devskiller.friendly-id friendly-id-project 1.1.1-SNAPSHOT .. - 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 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 59bd4f5..f5c73b9 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 @@ -32,6 +32,8 @@ public class FriendlyIdConfiguration implements WebMvcConfigurer { public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToUuidConverter()); registry.addConverter(new UuidToStringConverter()); + registry.addConverter(new StringToFriendlyIdConverter()); + registry.addConverter(new FriendlyIdToStringConverter()); } @Bean @@ -68,4 +70,34 @@ public String convert(UUID id) { return FriendlyId.toFriendlyId(id); } } + + /** + * Converter that converts FriendlyId strings to FriendlyId value objects. + *

+ * This converter is automatically registered in Spring's conversion service + * and allows path variables and request parameters to be automatically converted + * from FriendlyId format to FriendlyId value object. + */ + public static class StringToFriendlyIdConverter implements Converter { + + @Override + public com.devskiller.friendly_id.type.FriendlyId convert(String id) { + return com.devskiller.friendly_id.type.FriendlyId.fromString(id); + } + } + + /** + * Converter that converts FriendlyId value objects to FriendlyId strings. + *

+ * This converter is automatically registered in Spring's conversion service + * and allows FriendlyId value objects to be automatically converted to FriendlyId format + * in responses and URL generation. + */ + public static class FriendlyIdToStringConverter implements Converter { + + @Override + public String convert(com.devskiller.friendly_id.type.FriendlyId id) { + return id.toString(); + } + } } diff --git a/friendly-id/pom.xml b/friendly-id/pom.xml index 27cd7ed..0bb73a5 100644 --- a/friendly-id/pom.xml +++ b/friendly-id/pom.xml @@ -12,6 +12,9 @@ friendly-id + FriendlyId Core + Core library for converting UUIDs to URL-friendly Base62-encoded IDs + org.junit.jupiter diff --git a/pom.xml b/pom.xml index cdc53b6..16e67df 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ friendly-id-jackson-datatype friendly-id-jooq friendly-id-jpa + friendly-id-openfeign friendly-id-spring-boot friendly-id-spring-boot-starter friendly-id-samples @@ -28,6 +29,7 @@ 21 21 3.4.1 + 2023.0.3 @@ -102,6 +104,13 @@ 3.27.3 test + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + From 7bda4674cd51dd2bbe81ca2e4368b7d17242c7a4 Mon Sep 17 00:00:00 2001 From: "Mariusz S." Date: Fri, 3 Oct 2025 21:01:07 +0200 Subject: [PATCH 06/27] chore: implement CI-friendly Maven versioning and automated releases - Configure CI-friendly versioning using ${revision} property - Update all modules to use ${revision} instead of hardcoded version - Add revision property (1.1.1-SNAPSHOT) to parent and sample POMs - Create GitHub Actions workflow for automated releases on tag push - Update CI workflow to use Java 21 and modern actions (v4) - Enable version override via CLI: mvn -Drevision=X.Y.Z Release process: 1. Create tag: git tag -a vX.Y.Z -m "Release X.Y.Z" 2. Push tag: git push origin vX.Y.Z 3. GitHub Actions automatically builds and deploys to Maven Central Closes #[issue-number] if applicable --- .github/workflows/maven.yml | 31 ++++++++---- .github/workflows/release.yml | 49 +++++++++++++++++++ friendly-id-jackson-datatype/pom.xml | 2 +- friendly-id-jooq/pom.xml | 2 +- friendly-id-jpa/pom.xml | 2 +- friendly-id-openfeign/pom.xml | 2 +- .../friendly-id-contracts/pom.xml | 3 +- .../pom.xml | 3 +- .../friendly-id-spring-boot-hateos/pom.xml | 3 +- .../friendly-id-spring-boot-simple/pom.xml | 3 +- friendly-id-samples/pom.xml | 2 +- friendly-id-spring-boot-starter/pom.xml | 2 +- friendly-id-spring-boot/pom.xml | 2 +- friendly-id/pom.xml | 2 +- pom.xml | 5 +- 15 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/release.yml 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..9c30d8c --- /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: ossrh + 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/friendly-id-jackson-datatype/pom.xml b/friendly-id-jackson-datatype/pom.xml index 598e61c..74b83d7 100644 --- a/friendly-id-jackson-datatype/pom.xml +++ b/friendly-id-jackson-datatype/pom.xml @@ -5,7 +5,7 @@ friendly-id-project com.devskiller.friendly-id - 1.1.1-SNAPSHOT + ${revision} .. diff --git a/friendly-id-jooq/pom.xml b/friendly-id-jooq/pom.xml index bf1802d..7696bf3 100644 --- a/friendly-id-jooq/pom.xml +++ b/friendly-id-jooq/pom.xml @@ -5,7 +5,7 @@ friendly-id-project com.devskiller.friendly-id - 1.1.1-SNAPSHOT + ${revision} .. diff --git a/friendly-id-jpa/pom.xml b/friendly-id-jpa/pom.xml index 12dbd6c..f079101 100644 --- a/friendly-id-jpa/pom.xml +++ b/friendly-id-jpa/pom.xml @@ -5,7 +5,7 @@ friendly-id-project com.devskiller.friendly-id - 1.1.1-SNAPSHOT + ${revision} .. diff --git a/friendly-id-openfeign/pom.xml b/friendly-id-openfeign/pom.xml index b7a5824..02772eb 100644 --- a/friendly-id-openfeign/pom.xml +++ b/friendly-id-openfeign/pom.xml @@ -5,7 +5,7 @@ friendly-id-project com.devskiller.friendly-id - 1.1.1-SNAPSHOT + ${revision} .. diff --git a/friendly-id-samples/friendly-id-contracts/pom.xml b/friendly-id-samples/friendly-id-contracts/pom.xml index d599549..ebd2208 100644 --- a/friendly-id-samples/friendly-id-contracts/pom.xml +++ b/friendly-id-samples/friendly-id-contracts/pom.xml @@ -5,7 +5,7 @@ com.devskiller.friendly-id spring-boot-contracts - 1.1.1-SNAPSHOT + ${revision} org.springframework.boot @@ -15,6 +15,7 @@ + 1.1.1-SNAPSHOT UTF-8 UTF-8 21 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 c361857..bb6f5f6 100644 --- a/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml @@ -5,7 +5,7 @@ com.devskiller.friendly-id spring-boot-customized - 1.1.1-SNAPSHOT + ${revision} org.springframework.boot @@ -15,6 +15,7 @@ + 1.1.1-SNAPSHOT UTF-8 UTF-8 21 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 e90ab0f..f69e393 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml @@ -5,7 +5,7 @@ com.devskiller.friendly-id spring-boot-hateos - 1.1.1-SNAPSHOT + ${revision} org.springframework.boot @@ -15,6 +15,7 @@ + 1.1.1-SNAPSHOT UTF-8 UTF-8 21 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 e7f5552..a576797 100644 --- a/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml @@ -5,7 +5,7 @@ com.devskiller.friendly-id spring-boot-simple - 1.1.1-SNAPSHOT + ${revision} org.springframework.boot @@ -15,6 +15,7 @@ + 1.1.1-SNAPSHOT UTF-8 UTF-8 21 diff --git a/friendly-id-samples/pom.xml b/friendly-id-samples/pom.xml index 9f7a0f4..e56e165 100644 --- a/friendly-id-samples/pom.xml +++ b/friendly-id-samples/pom.xml @@ -8,7 +8,7 @@ com.devskiller.friendly-id friendly-id-project - 1.1.1-SNAPSHOT + ${revision} .. diff --git a/friendly-id-spring-boot-starter/pom.xml b/friendly-id-spring-boot-starter/pom.xml index 6612b0d..5ecb78b 100644 --- a/friendly-id-spring-boot-starter/pom.xml +++ b/friendly-id-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ com.devskiller.friendly-id friendly-id-project - 1.1.1-SNAPSHOT + ${revision} .. diff --git a/friendly-id-spring-boot/pom.xml b/friendly-id-spring-boot/pom.xml index cb74800..74f5879 100644 --- a/friendly-id-spring-boot/pom.xml +++ b/friendly-id-spring-boot/pom.xml @@ -5,7 +5,7 @@ com.devskiller.friendly-id friendly-id-project - 1.1.1-SNAPSHOT + ${revision} .. diff --git a/friendly-id/pom.xml b/friendly-id/pom.xml index 0bb73a5..29d57e9 100644 --- a/friendly-id/pom.xml +++ b/friendly-id/pom.xml @@ -6,7 +6,7 @@ com.devskiller.friendly-id friendly-id-project - 1.1.1-SNAPSHOT + ${revision} .. diff --git a/pom.xml b/pom.xml index 16e67df..14b2242 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 @@ -24,6 +24,9 @@ + + 1.1.1-SNAPSHOT + UTF-8 UTF-8 21 From 0d414b71bb4e8ade5d953ae22f10f229b2716775 Mon Sep 17 00:00:00 2001 From: "Mariusz S." Date: Fri, 3 Oct 2025 21:16:40 +0200 Subject: [PATCH 07/27] docs: add release process documentation --- RELEASING.md | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 RELEASING.md 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 From 5ddb28840d2cb154737d83dcfa8d42b482fee4bb Mon Sep 17 00:00:00 2001 From: "Mariusz S." Date: Fri, 3 Oct 2025 21:56:59 +0200 Subject: [PATCH 08/27] feat: add Spring Boot JPA demo and fix Jackson serialization - Add Spring Boot JPA demo application with FriendlyId value object - Add OpenFeign integration test demonstrating full FriendlyId flow - Fix Jackson module serialization by calling super.setupModule() - Add FriendlyId value object serializers/deserializers - Add friendly-id-jackson-datatype dependency to openfeign module - Add friendly-id-jackson-datatype dependency to spring-boot-starter - Update Spring Cloud version to 2024.0.2 in JPA demo - Remove OpenFeign auto-configuration to avoid circular dependencies --- CHANGELOG.md | 35 +++++ .../friendly_id/jackson/FriendlyIdModule.java | 5 + .../jackson/FriendlyIdValueDeserializer.java | 22 +++ .../jackson/FriendlyIdValueSerializer.java | 21 +++ friendly-id-openfeign/pom.xml | 5 + .../openfeign/FriendlyIdConfiguration.java | 49 +++--- ...ot.autoconfigure.AutoConfiguration.imports | 1 - .../friendly-id-spring-boot-jpa-demo/pom.xml | 114 ++++++++++++++ .../jpa/FriendlyIdJpaDemoApplication.java | 89 +++++++++++ .../friendly_id/sample/jpa/Product.java | 91 +++++++++++ .../sample/jpa/ProductController.java | 143 ++++++++++++++++++ .../sample/jpa/ProductRepository.java | 24 +++ .../sample/jpa/ProductRequest.java | 14 ++ .../src/main/resources/application.properties | 25 +++ .../friendly_id/sample/jpa/ProductClient.java | 29 ++++ .../jpa/ProductClientIntegrationTest.java | 90 +++++++++++ friendly-id-samples/pom.xml | 1 + friendly-id-spring-boot-starter/pom.xml | 5 + pom.xml | 2 +- 19 files changed, 742 insertions(+), 23 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java create mode 100644 friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java delete mode 100644 friendly-id-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 friendly-id-samples/friendly-id-spring-boot-jpa-demo/pom.xml create mode 100644 friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/FriendlyIdJpaDemoApplication.java create mode 100644 friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/Product.java create mode 100644 friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductController.java create mode 100644 friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductRepository.java create mode 100644 friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductRequest.java create mode 100644 friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/resources/application.properties create mode 100644 friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClient.java create mode 100644 friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClientIntegrationTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..116f39c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# 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 + +## [1.1.0] - Previous version +- Legacy implementation with UUID-only support 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..418a1f2 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 @@ -12,10 +12,15 @@ public FriendlyIdModule() { introspector = new FriendlyIdAnnotationIntrospector(); addDeserializer(UUID.class, new FriendlyIdDeserializer()); addSerializer(UUID.class, new FriendlyIdSerializer()); + + // Add serializer/deserializer for FriendlyId value object + addDeserializer(com.devskiller.friendly_id.type.FriendlyId.class, new FriendlyIdValueDeserializer()); + addSerializer(com.devskiller.friendly_id.type.FriendlyId.class, new FriendlyIdValueSerializer()); } @Override public void setupModule(SetupContext context) { + super.setupModule(context); context.insertAnnotationIntrospector(introspector); } } 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..569d0d1 --- /dev/null +++ b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java @@ -0,0 +1,22 @@ +package com.devskiller.friendly_id.jackson; + +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-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..627ed51 --- /dev/null +++ b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java @@ -0,0 +1,21 @@ +package com.devskiller.friendly_id.jackson; + +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-openfeign/pom.xml b/friendly-id-openfeign/pom.xml index 02772eb..78213c1 100644 --- a/friendly-id-openfeign/pom.xml +++ b/friendly-id-openfeign/pom.xml @@ -20,6 +20,11 @@ friendly-id ${project.version} + + com.devskiller.friendly-id + friendly-id-jackson-datatype + ${project.version} + org.springframework.cloud spring-cloud-starter-openfeign 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 index 2942549..f634139 100644 --- 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 @@ -2,8 +2,11 @@ import java.util.UUID; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.support.SpringEncoder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -11,46 +14,50 @@ import feign.codec.Encoder; /** - * Auto-configuration for FriendlyId integration with Spring Cloud OpenFeign. + * Configuration for FriendlyId integration with Spring Cloud OpenFeign. *

- * This configuration automatically registers custom encoder and decoder that handle - * FriendlyId conversion in Feign clients: + * This configuration can be used with {@code @FeignClient} to enable FriendlyId support: *

- *
    - *
  • Encoder: Converts UUID/FriendlyId parameters to FriendlyId strings in requests
  • - *
  • Decoder: Converts FriendlyId strings to UUID/FriendlyId in responses
  • - *
- * - *

Usage Example

*
{@code
- * @FeignClient(name = "user-service")
+ * @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 com.devskiller.friendly_id.type.FriendlyId id);
+ *     ProfileDto getProfile(@PathVariable FriendlyId id);
  * }
- *
- * // Usage
- * UUID userId = UUID.randomUUID();
- * UserDto user = userClient.getUser(userId); // Sends as FriendlyId string
  * }
+ *

+ * 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 */ -@Configuration -@ConditionalOnClass(FeignClient.class) 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 - public Encoder friendlyIdEncoder(Encoder defaultEncoder) { - return new FriendlyIdEncoder(defaultEncoder); + public Encoder feignEncoder() { + return new FriendlyIdEncoder(new SpringEncoder(() -> new HttpMessageConverters())); } + /** + * 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 - public Decoder friendlyIdDecoder(Decoder defaultDecoder) { - return new FriendlyIdDecoder(defaultDecoder); + public Decoder feignDecoder() { + return new FriendlyIdDecoder(new org.springframework.cloud.openfeign.support.SpringDecoder(() -> new HttpMessageConverters())); } } diff --git a/friendly-id-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/friendly-id-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 308d1cf..0000000 --- a/friendly-id-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -com.devskiller.friendly_id.openfeign.FriendlyIdConfiguration 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..9a3b4da --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + com.devskiller.friendly-id + spring-boot-jpa-demo + ${revision} + + + org.springframework.boot + spring-boot-starter-parent + 3.4.10 + + + + + 1.1.1-SNAPSHOT + UTF-8 + UTF-8 + 21 + 2024.0.2 + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + 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} + + + + + com.devskiller.friendly-id + friendly-id-openfeign + ${project.version} + test + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.cloud + spring-cloud-starter-openfeign + 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..b973b38 --- /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,89 @@ +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("\n========================================"); + System.out.println("Initializing demo products..."); + System.out.println("========================================\n"); + + Product 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()); + + Product 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()); + + Product 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("\n========================================"); + System.out.println("Demo ready!"); + System.out.println("========================================"); + System.out.println("REST API: http://localhost:8080/api/products"); + System.out.println("H2 Console: http://localhost:8080/h2-console"); + System.out.println(" JDBC URL: jdbc:h2:mem:friendlyid_demo"); + System.out.println(" Username: sa"); + System.out.println(" Password: (empty)"); + System.out.println("========================================\n"); + + System.out.println("Try these commands:"); + System.out.println(" curl http://localhost:8080/api/products"); + System.out.println(" curl http://localhost:8080/api/products/" + laptop.getId()); + System.out.println("\n"); + }; + } +} 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/ProductClient.java b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClient.java new file mode 100644 index 0000000..50764cb --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClient.java @@ -0,0 +1,29 @@ +package com.devskiller.friendly_id.sample.jpa; + +import java.util.List; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import com.devskiller.friendly_id.type.FriendlyId; + +/** + * OpenFeign client for testing FriendlyId integration. + *

+ * Demonstrates that FriendlyId works seamlessly with OpenFeign: + *

+ *
    + *
  • @PathVariable FriendlyId is automatically converted to FriendlyId string in URL
  • + *
  • Response JSON with FriendlyId strings is automatically deserialized to FriendlyId objects
  • + *
+ */ +@FeignClient(name = "productClient", url = "http://localhost:${server.port}", configuration = com.devskiller.friendly_id.openfeign.FriendlyIdConfiguration.class) +public interface ProductClient { + + @GetMapping("/api/products") + List getAllProducts(); + + @GetMapping("/api/products/{id}") + Product getProductById(@PathVariable FriendlyId id); +} 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..aaa9b8a --- /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,90 @@ +package com.devskiller.friendly_id.sample.jpa; + +import java.math.BigDecimal; +import java.util.List; + +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.cloud.openfeign.EnableFeignClients; + +import com.devskiller.friendly_id.type.FriendlyId; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test demonstrating FriendlyId usage with OpenFeign. + *

+ * 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. + *
  7. OpenFeign client automatically handles FriendlyId conversion
  8. + *
+ */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@EnableFeignClients +class ProductClientIntegrationTest { + + @Autowired + private ProductRepository repository; + + @Autowired + private ProductClient client; + + 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 shouldRetrieveAllProductsViaOpenFeign() { + List products = client.getAllProducts(); + + assertThat(products).hasSize(1); + assertThat(products.get(0).getId()).isEqualTo(testProduct.getId()); + assertThat(products.get(0).getName()).isEqualTo("Test Product"); + } + + @Test + void shouldRetrieveProductByFriendlyIdViaOpenFeign() { + FriendlyId productId = testProduct.getId(); + + Product retrievedProduct = client.getProductById(productId); + + assertThat(retrievedProduct).isNotNull(); + assertThat(retrievedProduct.getId()).isEqualTo(productId); + assertThat(retrievedProduct.getName()).isEqualTo("Test Product"); + assertThat(retrievedProduct.getDescription()).isEqualTo("Product for integration testing"); + assertThat(retrievedProduct.getPrice()).isEqualByComparingTo("99.99"); + assertThat(retrievedProduct.getStock()).isEqualTo(10); + } + + @Test + void shouldHandleFriendlyIdConversionInUrlPath() { + // This test verifies that FriendlyId in @PathVariable is correctly: + // 1. Converted to string in the URL by OpenFeign encoder + // 2. Parsed from string by Spring MVC converter + // 3. Used to query the database + // 4. Serialized to JSON string in response + // 5. Deserialized by OpenFeign decoder back to FriendlyId object + + Product product = client.getProductById(testProduct.getId()); + + assertThat(product.getId()).isEqualTo(testProduct.getId()); + assertThat(product.getId().toString()).matches("[0-9A-Za-z]{21,22}"); + } +} diff --git a/friendly-id-samples/pom.xml b/friendly-id-samples/pom.xml index e56e165..4dd24bb 100644 --- a/friendly-id-samples/pom.xml +++ b/friendly-id-samples/pom.xml @@ -16,6 +16,7 @@ friendly-id-spring-boot-simple friendly-id-spring-boot-customized friendly-id-spring-boot-hateos + friendly-id-spring-boot-jpa-demo friendly-id-contracts
diff --git a/friendly-id-spring-boot-starter/pom.xml b/friendly-id-spring-boot-starter/pom.xml index 5ecb78b..18f3d2b 100644 --- a/friendly-id-spring-boot-starter/pom.xml +++ b/friendly-id-spring-boot-starter/pom.xml @@ -20,6 +20,11 @@ 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/pom.xml b/pom.xml index 14b2242..39e8ea9 100644 --- a/pom.xml +++ b/pom.xml @@ -123,7 +123,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + 3.14.0 maven-surefire-plugin From 1b3a32845a72bdad60147401daa29e40638917c7 Mon Sep 17 00:00:00 2001 From: "Mariusz S." Date: Fri, 3 Oct 2025 22:18:14 +0200 Subject: [PATCH 09/27] chore: configure Maven Central publishing via Central Portal - Update distributionManagement to use Central Portal API - Replace nexus-staging-maven-plugin with central-publishing-maven-plugin - Add PUBLISHING.md with detailed publication instructions - Update CHANGELOG.md with infrastructure changes Migration from legacy OSSRH to new Central Portal system. Publishing configured but not executed - requires manual release. --- CHANGELOG.md | 4 ++ PUBLISHING.md | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++ pom.xml | 33 ++++++----- 3 files changed, 183 insertions(+), 14 deletions(-) create mode 100644 PUBLISHING.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 116f39c..6e73095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,5 +31,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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/pom.xml b/pom.xml index 39e8ea9..ae16df3 100644 --- a/pom.xml +++ b/pom.xml @@ -60,16 +60,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 + @@ -181,6 +181,11 @@ coveralls-maven-plugin 4.3.0 + + org.sonatype.central + central-publishing-maven-plugin + 0.6.0 + @@ -254,14 +259,14 @@
- org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 + org.sonatype.central + central-publishing-maven-plugin + 0.6.0 true - sonatype-nexus-staging - https://oss.sonatype.org/ - true + central + true + published From b8607159b63eafe2e0f7f4b79ac2256c7f0bfa8a Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sat, 20 Dec 2025 21:55:23 +0100 Subject: [PATCH 10/27] feat: upgrade to Spring Boot 4.0.1 and Jackson 3 - Upgrade Spring Boot from 3.4.1 to 4.0.1 - Upgrade Spring Cloud to 2025.0.0 - Migrate Jackson from com.fasterxml.jackson to tools.jackson (Jackson 3) - Update FriendlyIdModule to use SimpleModule instead of JacksonModule - Refactor serializers/deserializers for Jackson 3 API - Use createContextual for @IdFormat annotation support - Update OpenFeign config for new HttpMessageConverters location - Update sample projects for Spring Boot 4 test packages - Bump version to 2.0.0-SNAPSHOT for major version upgrade --- friendly-id-jackson-datatype/pom.xml | 10 ++-- .../FriendlyIdAnnotationIntrospector.java | 60 ------------------- .../jackson/FriendlyIdDeserializer.java | 52 ++++++++++++---- .../friendly_id/jackson/FriendlyIdModule.java | 31 +++++----- .../jackson/FriendlyIdSerializer.java | 35 +++++++++-- .../jackson/FriendlyIdValueDeserializer.java | 18 +++--- .../jackson/FriendlyIdValueSerializer.java | 16 ++--- .../spring/FieldWithoutFriendlyIdTest.java | 35 +++++------ .../spring/FriendlyIdDeserializerTest.java | 4 +- .../spring/ObjectMapperConfiguration.java | 16 ++--- friendly-id-openfeign/pom.xml | 11 ++++ .../openfeign/FriendlyIdConfiguration.java | 23 +++---- .../friendly-id-contracts/pom.xml | 19 ++---- .../friendly_id/sample/contracts/MvcTest.java | 34 ++++------- .../pom.xml | 14 +++-- .../sample/customized/ApplicationTest.java | 2 +- .../friendly-id-spring-boot-hateos/pom.xml | 22 +++---- .../sample/hateos/BarControllerTest.java | 2 +- .../sample/hateos/FooControllerTest.java | 2 +- .../friendly-id-spring-boot-jpa-demo/pom.xml | 6 +- .../friendly-id-spring-boot-simple/pom.xml | 9 ++- .../sample/simple/ApplicationTest.java | 4 +- friendly-id-spring-boot/pom.xml | 6 ++ .../spring/FriendlyIdConfiguration.java | 4 +- pom.xml | 11 ++-- 25 files changed, 221 insertions(+), 225 deletions(-) delete mode 100644 friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java diff --git a/friendly-id-jackson-datatype/pom.xml b/friendly-id-jackson-datatype/pom.xml index 74b83d7..c6eb295 100644 --- a/friendly-id-jackson-datatype/pom.xml +++ b/friendly-id-jackson-datatype/pom.xml @@ -20,22 +20,20 @@ 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 - org.junit.jupiter diff --git a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java b/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java deleted file mode 100644 index 3f82115..0000000 --- a/friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.devskiller.friendly_id.jackson; - -import java.util.UUID; - -import com.fasterxml.jackson.databind.deser.std.UUIDDeserializer; -import com.fasterxml.jackson.databind.introspect.Annotated; -import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; -import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; -import com.fasterxml.jackson.databind.ser.std.UUIDSerializer; - -public class FriendlyIdAnnotationIntrospector extends JacksonAnnotationIntrospector { - - private static final long serialVersionUID = 1L; - - @Override - 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 FriendlyIdSerializer.class; - } else { - return null; - } - } - - @Override - public Object findDeserializer(Annotated annotatedMethod) { - IdFormat 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 FriendlyIdDeserializer.class; - } else { - return null; - } - } - - private Class rawDeserializationType(Annotated a) { - if (a instanceof AnnotatedMethod) { - AnnotatedMethod am = (AnnotatedMethod) a; - if (am.getParameterCount() == 1) { - return am.getRawParameterType(0); - } - } - return a.getRawType(); - } -} 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..53af9b7 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,27 +1,42 @@ 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; -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 deserializationContext) { + JsonToken token = parser.currentToken(); if (token == JsonToken.VALUE_STRING) { - String string = parser.getValueAsString().trim(); - if (looksLikeUuid(string)) { - return super.deserialize(parser, deserializationContext); + String string = parser.getString().trim(); + if (useFriendlyFormat) { + if (looksLikeUuid(string)) { + return UUID.fromString(string); + } else { + return FriendlyId.toUuid(string); + } } else { - return FriendlyId.toUuid(string); + return UUID.fromString(string); } } throw new IllegalStateException("This is not friendly id"); @@ -30,4 +45,15 @@ public UUID deserialize(JsonParser parser, DeserializationContext deserializatio private boolean looksLikeUuid(String value) { return value.contains("-"); } + + @Override + public ValueDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { + if (property != null) { + IdFormat 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 418a1f2..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,25 +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()); - addSerializer(UUID.class, new FriendlyIdSerializer()); + super("FriendlyIdModule"); - // Add serializer/deserializer for FriendlyId value object - addDeserializer(com.devskiller.friendly_id.type.FriendlyId.class, new FriendlyIdValueDeserializer()); - addSerializer(com.devskiller.friendly_id.type.FriendlyId.class, new FriendlyIdValueSerializer()); - } + // UUID serializers/deserializers + addSerializer(UUID.class, new FriendlyIdSerializer()); + addDeserializer(UUID.class, new FriendlyIdDeserializer()); - @Override - public void setupModule(SetupContext context) { - super.setupModule(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..c5b1c83 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,45 @@ 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; 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 index 569d0d1..1a515c8 100644 --- 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 @@ -1,10 +1,8 @@ package com.devskiller.friendly_id.jackson; -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.deser.std.StdDeserializer; import com.devskiller.friendly_id.type.FriendlyId; @@ -12,11 +10,15 @@ * JSON deserializer for {@link FriendlyId} value object. * Deserializes JSON strings to FriendlyId instances. */ -public class FriendlyIdValueDeserializer extends JsonDeserializer { +public class FriendlyIdValueDeserializer extends StdDeserializer { + + public FriendlyIdValueDeserializer() { + super(FriendlyId.class); + } @Override - public FriendlyId deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - String friendlyIdString = p.getValueAsString(); + 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 index 627ed51..b2d87fc 100644 --- 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 @@ -1,10 +1,8 @@ package com.devskiller.friendly_id.jackson; -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ser.std.StdSerializer; import com.devskiller.friendly_id.type.FriendlyId; @@ -12,10 +10,14 @@ * JSON serializer for {@link FriendlyId} value object. * Serializes FriendlyId instances as their string representation. */ -public class FriendlyIdValueSerializer extends JsonSerializer { +public class FriendlyIdValueSerializer extends StdSerializer { + + public FriendlyIdValueSerializer() { + super(FriendlyId.class); + } @Override - public void serialize(FriendlyId value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + public void serialize(FriendlyId value, JsonGenerator gen, SerializationContext ctxt) { gen.writeString(value.toString()); } } 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 aa99b6d..c678910 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,53 +2,50 @@ import java.util.UUID; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import tools.jackson.databind.json.JsonMapper; 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(); + private JsonMapper jsonMapper = mapper(); @Test - void shouldAllowToDoNotCodeUuidInDataObject() throws Exception { + void shouldAllowToDoNotCodeUuidInDataObject() { Foo foo = new Foo(); foo.setRawUuid(uuid); foo.setFriendlyId(uuid); - String json = mapper.writeValueAsString(foo); + String 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); + Foo cloned = jsonMapper.readValue(json, Foo.class); assertThat(cloned.getRawUuid()).isEqualTo(foo.getFriendlyId()); } @Test - void shouldDeserializeUuidsInDataObject() throws Exception { + void shouldDeserializeUuidsInDataObject() { String json = "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}"; - Foo cloned = mapper.readValue(json, Foo.class); + Foo cloned = jsonMapper.readValue(json, Foo.class); assertThat(cloned.getRawUuid()).isEqualTo(uuid); assertThat(cloned.getFriendlyId()).isEqualTo(uuid); } @Test - void shouldSerializeUuidsInValueObject() throws Exception { - mapper = mapper(new ParameterNamesModule()); + void shouldSerializeUuidsInValueObject() { + jsonMapper = mapper(); Bar bar = new Bar(uuid, uuid); - String json = mapper.writeValueAsString(bar); + String json = jsonMapper.writeValueAsString(bar); assertThat(json).isEqualToIgnoringWhitespace( "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}" @@ -56,12 +53,12 @@ void shouldSerializeUuidsInValueObject() throws Exception { } @Test - void shouldDeserializeUuuidsValueObject() throws Exception { - mapper = mapper(new ParameterNamesModule()); + void shouldDeserializeUuuidsValueObject() { + jsonMapper = mapper(); String json = "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}"; - Bar deserialized = mapper.readValue(json, Bar.class); + Bar deserialized = jsonMapper.readValue(json, Bar.class); assertThat(deserialized.getRawUuid()).isEqualTo(uuid); assertThat(deserialized.getFriendlyId()).isEqualTo(uuid); 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 e0525b3..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 @@ -12,7 +12,7 @@ class FriendlyIdDeserializerTest { @Test - void shouldSerializeFriendlyId() throws Exception { + void shouldSerializeFriendlyId() { UUID uuid = UUID.randomUUID(); String json = mapper().writeValueAsString(uuid); System.out.println(json); @@ -20,7 +20,7 @@ void shouldSerializeFriendlyId() throws Exception { } @Test - 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-openfeign/pom.xml b/friendly-id-openfeign/pom.xml index 78213c1..3650707 100644 --- a/friendly-id-openfeign/pom.xml +++ b/friendly-id-openfeign/pom.xml @@ -30,6 +30,17 @@ spring-cloud-starter-openfeign provided
+ + org.springframework.boot + spring-boot-starter-web + provided + + + + org.springframework.boot + spring-boot-starter-classic + provided + 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 index f634139..84da235 100644 --- 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 @@ -1,14 +1,10 @@ package com.devskiller.friendly_id.openfeign; -import java.util.UUID; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.http.HttpMessageConverters; -import org.springframework.cloud.openfeign.FeignClient; +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 org.springframework.context.annotation.Configuration; import feign.codec.Decoder; import feign.codec.Encoder; @@ -39,6 +35,7 @@ * * @since 1.1.1 */ +@SuppressWarnings("deprecation") public class FriendlyIdConfiguration { /** @@ -47,8 +44,10 @@ public class FriendlyIdConfiguration { * UUID and FriendlyId objects to convert them to FriendlyId strings. */ @Bean - public Encoder feignEncoder() { - return new FriendlyIdEncoder(new SpringEncoder(() -> new HttpMessageConverters())); + @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)); } /** @@ -57,7 +56,9 @@ public Encoder feignEncoder() { * FriendlyId strings back to UUID or FriendlyId objects when needed. */ @Bean - public Decoder feignDecoder() { - return new FriendlyIdDecoder(new org.springframework.cloud.openfeign.support.SpringDecoder(() -> new HttpMessageConverters())); + @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-samples/friendly-id-contracts/pom.xml b/friendly-id-samples/friendly-id-contracts/pom.xml index ebd2208..42a3b28 100644 --- a/friendly-id-samples/friendly-id-contracts/pom.xml +++ b/friendly-id-samples/friendly-id-contracts/pom.xml @@ -10,16 +10,16 @@ org.springframework.boot spring-boot-starter-parent - 3.4.1 + 4.0.1 - 1.1.1-SNAPSHOT + 2.0.0-SNAPSHOT UTF-8 UTF-8 21 - 4.1.5 + 5.0.1 @@ -46,18 +46,7 @@ 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 - + org.projectlombok 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 index 13256e1..3aba10a 100644 --- 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 @@ -2,18 +2,16 @@ 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.jupiter.api.BeforeEach; 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.http.converter.json.JacksonHttpMessageConverter; +import org.springframework.http.converter.json.JsonMapperBuilder; import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; import java.util.UUID; @@ -29,7 +27,7 @@ public void setup() { mockMvcBuilder = standaloneSetup(new FooController(mock(EntityLinks.class))); DefaultFormattingConversionService service = new DefaultFormattingConversionService(); service.addConverter(new StringToUuidConverter()); - mockMvcBuilder.setMessageConverters(jackson2HttpMessageConverter()).setConversionService(service); + mockMvcBuilder.setMessageConverters(jacksonHttpMessageConverter()).setConversionService(service); RestAssuredMockMvc.standaloneSetup(mockMvcBuilder); } @@ -41,21 +39,15 @@ public UUID convert(String id) { } } - private MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() { - MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); - Jackson2ObjectMapperBuilder builder = this.jacksonBuilder(); - converter.setObjectMapper(builder.build()); - return converter; + private JacksonHttpMessageConverter jacksonHttpMessageConverter() { + JsonMapper mapper = jsonMapper(); + return new JacksonHttpMessageConverter(mapper); } - 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; + protected JsonMapper jsonMapper() { + return JsonMapper.builder() + .addModule(new FriendlyIdModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); } - - } 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 bb6f5f6..cc27f9d 100644 --- a/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml @@ -10,12 +10,12 @@ org.springframework.boot spring-boot-starter-parent - 3.4.1 + 4.0.1 - 1.1.1-SNAPSHOT + 2.0.0-SNAPSHOT UTF-8 UTF-8 21 @@ -32,10 +32,7 @@ ${project.version} - - com.fasterxml.jackson.module - jackson-module-parameter-names - + org.projectlombok @@ -48,6 +45,11 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-webmvc-test + test +
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 ff8ba2e..18cbc21 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 @@ -4,7 +4,7 @@ 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.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.http.MediaType; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; 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 f69e393..04c884a 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml @@ -10,12 +10,12 @@ org.springframework.boot spring-boot-starter-parent - 3.4.1 + 4.0.1 - 1.1.1-SNAPSHOT + 2.0.0-SNAPSHOT UTF-8 UTF-8 21 @@ -40,18 +40,7 @@ evo-inflector 1.3 - - com.fasterxml.jackson.module - jackson-module-parameter-names - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - + org.projectlombok @@ -64,6 +53,11 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-webmvc-test + test + 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 fbf4a2e..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 @@ -3,7 +3,7 @@ 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.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; 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 3d6f677..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 @@ -3,7 +3,7 @@ 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.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; 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 index 9a3b4da..2a96ed0 100644 --- a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/pom.xml @@ -9,16 +9,16 @@ org.springframework.boot spring-boot-starter-parent - 3.4.10 + 4.0.1 - 1.1.1-SNAPSHOT + 2.0.0-SNAPSHOT UTF-8 UTF-8 21 - 2024.0.2 + 2025.0.0 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 a576797..e56583f 100644 --- a/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml @@ -10,12 +10,12 @@ org.springframework.boot spring-boot-starter-parent - 3.4.1 + 4.0.1 - 1.1.1-SNAPSHOT + 2.0.0-SNAPSHOT UTF-8 UTF-8 21 @@ -37,6 +37,11 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-resttestclient + test + 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 12971a5..9304199 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,14 +2,16 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; import java.util.UUID; import static org.assertj.core.api.BDDAssertions.then; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestRestTemplate class ApplicationTest { @Autowired diff --git a/friendly-id-spring-boot/pom.xml b/friendly-id-spring-boot/pom.xml index 74f5879..da30023 100644 --- a/friendly-id-spring-boot/pom.xml +++ b/friendly-id-spring-boot/pom.xml @@ -25,6 +25,12 @@ 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/FriendlyIdConfiguration.java b/friendly-id-spring-boot/src/main/java/com/devskiller/friendly_id/spring/FriendlyIdConfiguration.java index f5c73b9..6e0828f 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,7 +2,7 @@ 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; @@ -37,7 +37,7 @@ public void addFormatters(FormatterRegistry registry) { } @Bean - public Module friendlyIdModule() { + public JacksonModule friendlyIdModule() { return new FriendlyIdModule(); } diff --git a/pom.xml b/pom.xml index ae16df3..310b772 100644 --- a/pom.xml +++ b/pom.xml @@ -25,14 +25,15 @@ - 1.1.1-SNAPSHOT - + 2.0.0-SNAPSHOT + UTF-8 UTF-8 21 21 - 3.4.1 - 2023.0.3 + 4.0.1 + 2025.0.0 + 3.20.1 @@ -94,7 +95,7 @@ org.jooq jooq - 3.19.16 + ${jooq.version} jakarta.persistence From 283e7a3ec7048ed8b95105258cceda1393a5f60e Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sat, 20 Dec 2025 22:10:32 +0100 Subject: [PATCH 11/27] fix: update sample tests for Spring Boot 4 API changes - Use new @WebMvcTest from org.springframework.boot.webmvc.test.autoconfigure - Replace JacksonHttpMessageConverter with JacksonJsonHttpMessageConverter - Add spring-boot-starter-webmvc-test dependency - Replace OpenFeign with MockMvc in JPA demo tests - Use MockMvc instead of deprecated TestRestTemplate --- .../friendly-id-contracts/pom.xml | 5 ++ .../sample/contracts/BarControllerTest.java | 2 +- .../contracts/ContractVerifierBase.java | 2 +- .../sample/contracts/FooControllerTest.java | 2 +- .../friendly_id/sample/contracts/MvcTest.java | 9 +-- .../friendly-id-spring-boot-jpa-demo/pom.xml | 26 +------ .../friendly_id/sample/jpa/ProductClient.java | 29 -------- .../jpa/ProductClientIntegrationTest.java | 72 ++++++++++--------- .../friendly-id-spring-boot-simple/pom.xml | 2 +- .../sample/simple/ApplicationTest.java | 33 +++++---- 10 files changed, 70 insertions(+), 112 deletions(-) delete mode 100644 friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClient.java diff --git a/friendly-id-samples/friendly-id-contracts/pom.xml b/friendly-id-samples/friendly-id-contracts/pom.xml index 42a3b28..26ae10b 100644 --- a/friendly-id-samples/friendly-id-contracts/pom.xml +++ b/friendly-id-samples/friendly-id-contracts/pom.xml @@ -59,6 +59,11 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-webmvc-test + test + 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 index b375c2b..a67d106 100644 --- 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 @@ -3,7 +3,7 @@ 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.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; 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 dba975a..9d2dde2 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 @@ -4,7 +4,7 @@ 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.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.web.context.WebApplicationContext; 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 index 1719e72..4881f13 100644 --- 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 @@ -3,7 +3,7 @@ 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.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; 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 index 3aba10a..2f5942f 100644 --- 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 @@ -7,10 +7,8 @@ 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.JacksonHttpMessageConverter; -import org.springframework.http.converter.json.JsonMapperBuilder; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; -import tools.jackson.databind.SerializationFeature; import tools.jackson.databind.json.JsonMapper; import java.util.UUID; @@ -39,15 +37,14 @@ public UUID convert(String id) { } } - private JacksonHttpMessageConverter jacksonHttpMessageConverter() { + private JacksonJsonHttpMessageConverter jacksonHttpMessageConverter() { JsonMapper mapper = jsonMapper(); - return new JacksonHttpMessageConverter(mapper); + return new JacksonJsonHttpMessageConverter(mapper); } protected JsonMapper jsonMapper() { return JsonMapper.builder() .addModule(new FriendlyIdModule()) - .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .build(); } } 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 index 2a96ed0..d636077 100644 --- a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/pom.xml @@ -18,21 +18,8 @@ UTF-8 UTF-8 21 - 2025.0.0 - - - - org.springframework.cloud - spring-cloud-dependencies - ${spring-cloud.version} - pom - import - - - - @@ -67,24 +54,15 @@ ${project.version} - - - com.devskiller.friendly-id - friendly-id-openfeign - ${project.version} - test - - org.springframework.boot spring-boot-starter-test test - - org.springframework.cloud - spring-cloud-starter-openfeign + org.springframework.boot + spring-boot-starter-webmvc-test test diff --git a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClient.java b/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClient.java deleted file mode 100644 index 50764cb..0000000 --- a/friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClient.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.devskiller.friendly_id.sample.jpa; - -import java.util.List; - -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; - -import com.devskiller.friendly_id.type.FriendlyId; - -/** - * OpenFeign client for testing FriendlyId integration. - *

- * Demonstrates that FriendlyId works seamlessly with OpenFeign: - *

- *
    - *
  • @PathVariable FriendlyId is automatically converted to FriendlyId string in URL
  • - *
  • Response JSON with FriendlyId strings is automatically deserialized to FriendlyId objects
  • - *
- */ -@FeignClient(name = "productClient", url = "http://localhost:${server.port}", configuration = com.devskiller.friendly_id.openfeign.FriendlyIdConfiguration.class) -public interface ProductClient { - - @GetMapping("/api/products") - List getAllProducts(); - - @GetMapping("/api/products/{id}") - Product getProductById(@PathVariable FriendlyId id); -} 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 index aaa9b8a..d6a8977 100644 --- 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 @@ -1,20 +1,23 @@ package com.devskiller.friendly_id.sample.jpa; import java.math.BigDecimal; -import java.util.List; 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.cloud.openfeign.EnableFeignClients; +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.assertj.core.api.Assertions.assertThat; +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 OpenFeign. + * Integration test demonstrating FriendlyId usage with JPA. *

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

@@ -22,25 +25,24 @@ *
  • Entity stored in database with FriendlyId as UUID
  • *
  • REST controller accepts FriendlyId in @PathVariable
  • *
  • JSON serialization converts FriendlyId to/from string
  • - *
  • OpenFeign client automatically handles FriendlyId conversion
  • * */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@EnableFeignClients +@SpringBootTest +@AutoConfigureMockMvc class ProductClientIntegrationTest { @Autowired private ProductRepository repository; @Autowired - private ProductClient client; + private MockMvc mockMvc; private Product testProduct; @BeforeEach void setUp() { repository.deleteAll(); - + testProduct = new Product( "Test Product", "Product for integration testing", @@ -51,40 +53,40 @@ void setUp() { } @Test - void shouldRetrieveAllProductsViaOpenFeign() { - List products = client.getAllProducts(); - - assertThat(products).hasSize(1); - assertThat(products.get(0).getId()).isEqualTo(testProduct.getId()); - assertThat(products.get(0).getName()).isEqualTo("Test Product"); + 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 shouldRetrieveProductByFriendlyIdViaOpenFeign() { + void shouldRetrieveProductByFriendlyId() throws Exception { FriendlyId productId = testProduct.getId(); - Product retrievedProduct = client.getProductById(productId); - - assertThat(retrievedProduct).isNotNull(); - assertThat(retrievedProduct.getId()).isEqualTo(productId); - assertThat(retrievedProduct.getName()).isEqualTo("Test Product"); - assertThat(retrievedProduct.getDescription()).isEqualTo("Product for integration testing"); - assertThat(retrievedProduct.getPrice()).isEqualByComparingTo("99.99"); - assertThat(retrievedProduct.getStock()).isEqualTo(10); + 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() { + void shouldHandleFriendlyIdConversionInUrlPath() throws Exception { // This test verifies that FriendlyId in @PathVariable is correctly: - // 1. Converted to string in the URL by OpenFeign encoder - // 2. Parsed from string by Spring MVC converter - // 3. Used to query the database - // 4. Serialized to JSON string in response - // 5. Deserialized by OpenFeign decoder back to FriendlyId object - - Product product = client.getProductById(testProduct.getId()); - - assertThat(product.getId()).isEqualTo(testProduct.getId()); - assertThat(product.getId().toString()).matches("[0-9A-Za-z]{21,22}"); + // 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 e56583f..7597461 100644 --- a/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-simple/pom.xml @@ -39,7 +39,7 @@ org.springframework.boot - spring-boot-resttestclient + spring-boot-starter-webmvc-test test 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 9304199..6cd9638 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 @@ -1,32 +1,37 @@ package com.devskiller.friendly_id.sample.simple; +import com.devskiller.friendly_id.FriendlyId; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; 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 java.util.UUID; -import static org.assertj.core.api.BDDAssertions.then; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureTestRestTemplate +@SpringBootTest +@AutoConfigureMockMvc class ApplicationTest { @Autowired - private TestRestTemplate restTemplate; + private MockMvc mockMvc; @Test - void shouldSerialize() { + void shouldSerialize() throws Exception { // given UUID uuid = UUID.randomUUID(); - - // when - Bar entity = restTemplate.getForEntity("/bars/{id}", Bar.class, uuid).getBody(); - - // then - then(entity).isNotNull(); - then(entity.getId()).isEqualTo(uuid); + String friendlyId = FriendlyId.toFriendlyId(uuid); + + // when/then + mockMvc.perform(get("/bars/{id}", friendlyId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", is(friendlyId))); } } From 1df0d4f5eeab8ef32a81679bc15934855ccdee33 Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sat, 20 Dec 2025 22:13:45 +0100 Subject: [PATCH 12/27] chore: update ciManagement to GitHub Actions --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 310b772..33944f8 100644 --- a/pom.xml +++ b/pom.xml @@ -79,8 +79,8 @@ - Travis - https://travis-ci.org/Devskiller/friendly-id + GitHub Actions + https://github.com/SkillPanel/friendly-id/actions From cbb57d4d028d3c26ae2640b6e12eaa3cc40fe927 Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sat, 20 Dec 2025 22:14:17 +0100 Subject: [PATCH 13/27] chore: remove .serena directory --- .serena/.gitignore | 1 - .serena/project.yml | 67 --------------------------------------------- 2 files changed, 68 deletions(-) delete mode 100644 .serena/.gitignore delete mode 100644 .serena/project.yml diff --git a/.serena/.gitignore b/.serena/.gitignore deleted file mode 100644 index 14d86ad..0000000 --- a/.serena/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/cache diff --git a/.serena/project.yml b/.serena/project.yml deleted file mode 100644 index 64cbcbf..0000000 --- a/.serena/project.yml +++ /dev/null @@ -1,67 +0,0 @@ -# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) -# * For C, use cpp -# * For JavaScript, use typescript -# Special requirements: -# * csharp: Requires the presence of a .sln file in the project folder. -language: java - -# whether to use the project's gitignore file to ignore files -# Added on 2025-04-07 -ignore_all_files_in_gitignore: true -# list of additional paths to ignore -# same syntax as gitignore, so you can use * and ** -# Was previously called `ignored_dirs`, please update your config if you are using that. -# Added (renamed) on 2025-04-07 -ignored_paths: [] - -# whether the project is in read-only mode -# If set to true, all editing tools will be disabled and attempts to use them will result in an error -# Added on 2025-04-18 -read_only: false - -# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. -# Below is the complete list of tools for convenience. -# To make sure you have the latest list of tools, and to view their descriptions, -# execute `uv run scripts/print_tool_overview.py`. -# -# * `activate_project`: Activates a project by name. -# * `check_onboarding_performed`: Checks whether project onboarding was already performed. -# * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_lines`: Deletes a range of lines within a file. -# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. -# * `execute_shell_command`: Executes a shell command. -# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. -# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). -# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). -# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. -# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. -# * `initial_instructions`: Gets the initial instructions for the current project. -# Should only be used in settings where the system prompt cannot be set, -# e.g. in clients you have no control over, like Claude Desktop. -# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `insert_at_line`: Inserts content at a given line in a file. -# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. -# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). -# * `list_memories`: Lists memories in Serena's project-specific memory store. -# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). -# * `read_file`: Reads a file within the project directory. -# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. -# * `remove_project`: Removes a project from the Serena configuration. -# * `replace_lines`: Replaces a range of lines within a file with new content. -# * `replace_symbol_body`: Replaces the full definition of a symbol. -# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. -# * `search_for_pattern`: Performs a search for a pattern in the project. -# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. -# * `switch_modes`: Activates modes by providing a list of their names -# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. -# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. -# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. -# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. -excluded_tools: [] - -# initial prompt for the project. It will always be given to the LLM upon activating the project -# (contrary to the memories, which are loaded on demand). -initial_prompt: "" - -project_name: "friendly-id" From 18024adf622e6817012a4cd41a7c21ff30610976 Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sat, 20 Dec 2025 22:14:45 +0100 Subject: [PATCH 14/27] chore: remove CLAUDE.md --- CLAUDE.md | 163 ------------------------------------------------------ 1 file changed, 163 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 1cade99..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,163 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -FriendlyID is a Java library that converts UUIDs (36 characters) to URL-friendly Base62-encoded IDs (max 22 characters). Example: `c3587ec5-0976-497f-8374-61e0c2ea3da5` → `5wbwf6yUxVBcr48AMbz9cb` - -**Technology Stack:** -- Java 21 -- Spring Boot 3.4.1 -- JUnit 5 (Jupiter) -- Maven multi-module project - -## Building and Testing - -### Build Commands - -```bash -# Build entire project (skipping tests) -mvn clean install -DskipTests - -# Build only core modules (most common during development) -mvn clean install -DskipTests -pl friendly-id,friendly-id-jackson-datatype,friendly-id-spring-boot,friendly-id-spring-boot-starter -am - -# Full build with tests -mvn clean install - -# Build specific module -mvn clean install -pl friendly-id - -# Run tests for specific module -mvn test -pl friendly-id - -# Run single test class -mvn test -pl friendly-id -Dtest=FriendlyIdTest - -# Run single test method -mvn test -pl friendly-id -Dtest=FriendlyIdTest#shouldCreateValidIdsThatConformToUuidType4 -``` - -### Test Execution Notes - -- Core library tests use `@RepeatedTest` extensively (1000 iterations) to ensure robustness -- Sample projects require Spring Boot context startup, which takes ~1-2 seconds -- Tests are designed to be fast: full test suite runs in under 10 seconds - -## Architecture - -### Module Structure - -**Multi-module Maven project with 5 modules:** - -1. **`friendly-id`** (core library) - - Pure Java, no dependencies - - Main classes: `FriendlyId`, `Base62`, `Url62`, `UuidConverter` - - Conversion logic between UUID ↔ Base62 string - -2. **`friendly-id-jackson-datatype`** - - Jackson integration for JSON serialization/deserialization - - `FriendlyIdModule` - registers custom serializers for UUID fields - - Automatically converts UUID to FriendlyID in JSON output - -3. **`friendly-id-spring-boot`** - - Spring MVC integration - - `FriendlyIdConfiguration` - registers Spring converters - - Enables `@PathVariable UUID` to accept FriendlyID strings in URLs - - Implements `WebMvcConfigurer` to add formatters - -4. **`friendly-id-spring-boot-starter`** - - Auto-configuration for Spring Boot - - Uses `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` - - Just add to dependencies - no manual configuration needed - -5. **`friendly-id-samples`** - - Four example Spring Boot applications - - Demonstrates different integration scenarios - -### Key Design Patterns - -**UUID Conversion Flow:** -``` -UUID (128-bit) → BigInteger → Base62 String → FriendlyID -``` - -**Spring Integration Pattern:** -- Uses Spring's `Converter` interface for bidirectional conversion -- `StringToUuidConverter`: FriendlyID string → UUID (for @PathVariable) -- `UuidToStringConverter`: UUID → FriendlyID string (for JSON serialization) -- Jackson module handles JSON serialization separately - -**Auto-configuration:** -- Spring Boot 3 uses `AutoConfiguration.imports` instead of `spring.factories` -- Conditional on class presence: `@ConditionalOnExpression` with property flag -- Default enabled, can be disabled with: `com.devskiller.friendly_id.auto=false` - -### Important Implementation Details - -**Base62 Encoding:** -- Character set: `[0-9A-Za-z]` (62 characters) -- Leading zeros are ignored during decoding -- Maximum 22 characters for full 128-bit UUID -- Shorter UUIDs produce shorter FriendlyIDs - -**Spring Boot 3 Requirements:** -- Compiler must use `-parameters` flag for parameter name reflection -- All sample projects have `true` in maven-compiler-plugin -- Without this flag, `@PathVariable` will fail with parameter name error - -**Testing Strategy:** -- Property-based testing replaced with `@RepeatedTest(1000)` -- Random data generation using `BigInteger(128, new Random())` -- Reversibility tests: encode→decode→encode should equal original - -## Common Development Scenarios - -### Adding New Integration - -When adding support for a new framework (e.g., Micronaut, Quarkus): - -1. Create new module: `friendly-id-{framework}-integration` -2. Add converter/formatter registration for the framework -3. Register Jackson module if framework uses Jackson -4. Create sample project in `friendly-id-samples/` - -### Modifying Base62 Algorithm - -Core conversion logic is in `friendly-id/src/main/java/com/devskiller/friendly_id/Base62.java`: -- `encode(BigInteger)` - converts number to Base62 string -- `decode(String)` - converts Base62 string back to number -- Validate changes don't break 128-bit limit -- Ensure reversibility: all existing tests must pass - -### Testing Spring Boot Integration - -Sample projects demonstrate real-world usage: -- `friendly-id-spring-boot-simple` - basic REST controller -- `friendly-id-spring-boot-customized` - custom configuration -- `friendly-id-spring-boot-hateos` - HATEOAS integration -- `friendly-id-contracts` - Spring Cloud Contract testing - -## Project-Specific Conventions - -- Use conventional commits format (https://www.conventionalcommits.org/) -- All code comments must be in English -- Tests use JUnit 5 (not JUnit 4) - no `@RunWith`, use plain `@Test` -- Prefer `@RepeatedTest` over property-based testing libraries -- Maven compiler version: 3.13.0+ (for Java 21 support) - -## Migration Notes (Current State) - -This project has recently been upgraded: -- Java 8 → 21 -- Spring Boot 2.2.2 → 3.4.1 -- JUnit 4 → JUnit 5 -- Vavr property testing → JUnit 5 `@RepeatedTest` -- GitHub Actions workflow still references Java 1.8 (needs update) - -## Known Issues - -- FIXME in `FriendlyIdConfiguration`: `StringToUuidConverter` should be public for better extensibility -- Sample `friendly-id-spring-boot-hateos` may need Lombok annotation processor configuration -- Travis CI badge in README is outdated (project uses GitHub Actions) From ce89fff1b34d8801949557a060e56c552a3c3094 Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sat, 20 Dec 2025 22:33:17 +0100 Subject: [PATCH 15/27] fix: use correct server-id for Central Portal --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c30d8c..54130c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: java-version: '21' distribution: 'temurin' cache: 'maven' - server-id: ossrh + server-id: central server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} From dac5c5109b38a52c908c947e8ed9f61dbb80b3e8 Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sat, 20 Dec 2025 22:37:20 +0100 Subject: [PATCH 16/27] fix: skip publishing for samples module --- friendly-id-samples/pom.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/friendly-id-samples/pom.xml b/friendly-id-samples/pom.xml index 4dd24bb..e2af9a4 100644 --- a/friendly-id-samples/pom.xml +++ b/friendly-id-samples/pom.xml @@ -28,6 +28,19 @@ true + + maven-deploy-plugin + + true + + + + org.sonatype.central + central-publishing-maven-plugin + + true + + From 409064af950e3daffbe863aba5a95fc80a1ef5f3 Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sun, 21 Dec 2025 12:52:37 +0100 Subject: [PATCH 17/27] chore: add name to samples module --- friendly-id-samples/pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/friendly-id-samples/pom.xml b/friendly-id-samples/pom.xml index e2af9a4..c30ee6f 100644 --- a/friendly-id-samples/pom.xml +++ b/friendly-id-samples/pom.xml @@ -4,6 +4,8 @@ friendly-id-samples pom + FriendlyId Samples + Sample applications demonstrating FriendlyId usage com.devskiller.friendly-id From 7fc38d324c0bccf8a13de974c69b83ebfc40f92b Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sun, 21 Dec 2025 12:58:39 +0100 Subject: [PATCH 18/27] chore: update Maven plugins to latest versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - central-publishing-maven-plugin: 0.6.0 → 0.9.0 - maven-compiler-plugin: 3.14.0 → 3.14.1 - maven-surefire-plugin: 3.5.2 → 3.5.4 - maven-gpg-plugin: 3.2.5 → 3.2.8 --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 33944f8..4c3fea8 100644 --- a/pom.xml +++ b/pom.xml @@ -124,11 +124,11 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + 3.14.1 maven-surefire-plugin - 3.5.2 + 3.5.4 maven-install-plugin @@ -185,7 +185,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.6.0 + 0.9.0 @@ -222,7 +222,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.5 + 3.2.8 sign-artifacts @@ -262,7 +262,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.6.0 + 0.9.0 true central From 990b68ecd4bf5b05c6b6e8ce9904debbcd998cce Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sun, 21 Dec 2025 13:21:08 +0100 Subject: [PATCH 19/27] feat: add Jackson 2.x datatype module and Spring Boot 3 sample - Add friendly-id-jackson2-datatype module for Jackson 2.x compatibility - Add friendly-id-spring-boot3-simple sample demonstrating usage with Spring Boot 3.4.1 and Jackson 2.x - Update jackson-datatype module service file for Jackson 3.x --- ...e => tools.jackson.databind.JacksonModule} | 0 friendly-id-jackson2-datatype/pom.xml | 71 +++++++++++++++++++ .../FriendlyIdAnnotationIntrospector.java | 60 ++++++++++++++++ .../jackson/FriendlyIdDeserializer.java | 33 +++++++++ .../friendly_id/jackson/FriendlyIdFormat.java | 17 +++++ .../friendly_id/jackson/FriendlyIdModule.java | 26 +++++++ .../jackson/FriendlyIdSerializer.java | 22 ++++++ .../jackson/FriendlyIdValueDeserializer.java | 22 ++++++ .../jackson/FriendlyIdValueSerializer.java | 21 ++++++ .../friendly_id/jackson/IdFormat.java | 14 ++++ .../com.fasterxml.jackson.databind.Module | 1 + .../devskiller/friendly_id/spring/Bar.java | 29 ++++++++ .../spring/FieldWithoutFriendlyIdTest.java | 70 ++++++++++++++++++ .../devskiller/friendly_id/spring/Foo.java | 30 ++++++++ .../spring/FriendlyIdDeserializerTest.java | 28 ++++++++ .../spring/ObjectMapperConfiguration.java | 16 +++++ .../friendly-id-spring-boot3-simple/pom.xml | 67 +++++++++++++++++ .../sample/spring3/Application.java | 26 +++++++ .../friendly_id/sample/spring3/Bar.java | 23 ++++++ .../sample/spring3/FriendlyIdConfig.java | 43 +++++++++++ .../sample/spring3/ApplicationTest.java | 38 ++++++++++ friendly-id-samples/pom.xml | 1 + pom.xml | 1 + 23 files changed, 659 insertions(+) rename friendly-id-jackson-datatype/src/main/resources/META-INF/services/{com.fasterxml.jackson.databind.Module => tools.jackson.databind.JacksonModule} (100%) create mode 100644 friendly-id-jackson2-datatype/pom.xml create mode 100644 friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java create mode 100644 friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java create mode 100644 friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdFormat.java create mode 100644 friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java create mode 100644 friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdSerializer.java create mode 100644 friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java create mode 100644 friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java create mode 100644 friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/IdFormat.java create mode 100644 friendly-id-jackson2-datatype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module create mode 100644 friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/Bar.java create mode 100644 friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/FieldWithoutFriendlyIdTest.java create mode 100644 friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/Foo.java create mode 100644 friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/FriendlyIdDeserializerTest.java create mode 100644 friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/ObjectMapperConfiguration.java create mode 100644 friendly-id-samples/friendly-id-spring-boot3-simple/pom.xml create mode 100644 friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Application.java create mode 100644 friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Bar.java create mode 100644 friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/FriendlyIdConfig.java create mode 100644 friendly-id-samples/friendly-id-spring-boot3-simple/src/test/java/com/devskiller/friendly_id/sample/spring3/ApplicationTest.java 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-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-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java new file mode 100644 index 0000000..3f82115 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java @@ -0,0 +1,60 @@ +package com.devskiller.friendly_id.jackson; + +import java.util.UUID; + +import com.fasterxml.jackson.databind.deser.std.UUIDDeserializer; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.databind.ser.std.UUIDSerializer; + +public class FriendlyIdAnnotationIntrospector extends JacksonAnnotationIntrospector { + + private static final long serialVersionUID = 1L; + + @Override + 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 FriendlyIdSerializer.class; + } else { + return null; + } + } + + @Override + public Object findDeserializer(Annotated annotatedMethod) { + IdFormat 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 FriendlyIdDeserializer.class; + } else { + return null; + } + } + + private Class rawDeserializationType(Annotated a) { + if (a instanceof AnnotatedMethod) { + AnnotatedMethod am = (AnnotatedMethod) a; + if (am.getParameterCount() == 1) { + return am.getRawParameterType(0); + } + } + return a.getRawType(); + } +} diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java new file mode 100644 index 0000000..3e0d89a --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java @@ -0,0 +1,33 @@ +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 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/jackson/FriendlyIdFormat.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdFormat.java new file mode 100644 index 0000000..2fb1985 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdFormat.java @@ -0,0 +1,17 @@ +package com.devskiller.friendly_id.jackson; + +/** + * Friendly ID format + */ +public enum FriendlyIdFormat { + + /** + * Url62 encoded ID + */ + URL62, + + /** + * Leave this ID as is (without conversion) + */ + RAW +} diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java new file mode 100644 index 0000000..418a1f2 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java @@ -0,0 +1,26 @@ +package com.devskiller.friendly_id.jackson; + +import java.util.UUID; + +import com.fasterxml.jackson.databind.module.SimpleModule; + +public class FriendlyIdModule extends SimpleModule { + + private FriendlyIdAnnotationIntrospector introspector; + + public FriendlyIdModule() { + introspector = new FriendlyIdAnnotationIntrospector(); + addDeserializer(UUID.class, new FriendlyIdDeserializer()); + addSerializer(UUID.class, new FriendlyIdSerializer()); + + // Add serializer/deserializer for FriendlyId value object + addDeserializer(com.devskiller.friendly_id.type.FriendlyId.class, new FriendlyIdValueDeserializer()); + addSerializer(com.devskiller.friendly_id.type.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/jackson/FriendlyIdSerializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdSerializer.java new file mode 100644 index 0000000..f80b803 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdSerializer.java @@ -0,0 +1,22 @@ +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 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/jackson/FriendlyIdValueDeserializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java new file mode 100644 index 0000000..569d0d1 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java @@ -0,0 +1,22 @@ +package com.devskiller.friendly_id.jackson; + +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/jackson/FriendlyIdValueSerializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java new file mode 100644 index 0000000..627ed51 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java @@ -0,0 +1,21 @@ +package com.devskiller.friendly_id.jackson; + +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/java/com/devskiller/friendly_id/jackson/IdFormat.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/IdFormat.java new file mode 100644 index 0000000..999c366 --- /dev/null +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/IdFormat.java @@ -0,0 +1,14 @@ +package com.devskiller.friendly_id.jackson; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Declares that a field should be formatted as a friendly ID. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface IdFormat { + + FriendlyIdFormat value() default FriendlyIdFormat.URL62; + +} 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..781a57d --- /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.jackson.FriendlyIdModule 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..ba9fb63 --- /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.jackson.FriendlyIdFormat; +import com.devskiller.friendly_id.jackson.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..8c96109 --- /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.jackson.FriendlyIdFormat; +import com.devskiller.friendly_id.jackson.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..e200068 --- /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.jackson.FriendlyIdModule; + +public class ObjectMapperConfiguration { + + protected static ObjectMapper mapper(Module... modules) { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new FriendlyIdModule()); + mapper.registerModules(modules); + return mapper; + } +} 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..d42256a --- /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,26 @@ +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.RestController; + +@RestController +@SpringBootApplication +public class Application { + + 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; + } + +} diff --git a/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Bar.java b/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Bar.java new file mode 100644 index 0000000..9192653 --- /dev/null +++ b/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Bar.java @@ -0,0 +1,23 @@ +package com.devskiller.friendly_id.sample.spring3; + +import java.util.UUID; + +public class Bar { + + private UUID id; + + public Bar() { + } + + public Bar(UUID id) { + this.id = id; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } +} 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..5e347e0 --- /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.jackson.FriendlyIdModule; + +@Configuration +public class FriendlyIdConfig implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new StringToUuidConverter()); + registry.addConverter(new UuidToStringConverter()); + } + + @Bean + FriendlyIdModule friendlyIdModule() { + return new FriendlyIdModule(); + } + + 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/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..cbb30bf --- /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,38 @@ +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.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +class ApplicationTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void shouldSerialize() throws Exception { + // given + UUID uuid = UUID.randomUUID(); + String friendlyId = FriendlyId.toFriendlyId(uuid); + + // when/then + mockMvc.perform(get("/bars/{id}", friendlyId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id", is(friendlyId))); + } +} diff --git a/friendly-id-samples/pom.xml b/friendly-id-samples/pom.xml index c30ee6f..d423c64 100644 --- a/friendly-id-samples/pom.xml +++ b/friendly-id-samples/pom.xml @@ -16,6 +16,7 @@ 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 diff --git a/pom.xml b/pom.xml index 4c3fea8..9c752ba 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,7 @@ friendly-id friendly-id-jackson-datatype + friendly-id-jackson2-datatype friendly-id-jooq friendly-id-jpa friendly-id-openfeign From 60a2744a2eb8d3e2eaa6271fec1234b6f6d40e2c Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sun, 21 Dec 2025 13:28:35 +0100 Subject: [PATCH 20/27] refactor: modernize code for Java 21 and Spring Boot 4 - Convert test DTOs (Bar, Foo) to records - Use pattern matching in FriendlyId.equals() - Replace inner converter classes with lambdas and static imports - Improve UUID format detection with proper validation - Use text blocks and var keyword throughout - Simplify Application controllers to use record constructors --- .../jackson/FriendlyIdDeserializer.java | 40 ++++++---- .../devskiller/friendly_id/spring/Bar.java | 26 +------ .../spring/FieldWithoutFriendlyIdTest.java | 41 +++++----- .../devskiller/friendly_id/spring/Foo.java | 27 +------ .../jpa/FriendlyIdJpaDemoApplication.java | 44 ++++++----- .../sample/simple/Application.java | 5 +- .../friendly_id/sample/simple/Bar.java | 20 +---- .../spring/FriendlyIdConfiguration.java | 75 +++---------------- .../friendly_id/type/FriendlyId.java | 5 +- 9 files changed, 91 insertions(+), 192 deletions(-) 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 53af9b7..b3f71d4 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 @@ -25,31 +25,45 @@ private FriendlyIdDeserializer(boolean useFriendlyFormat) { } @Override - public UUID deserialize(JsonParser parser, DeserializationContext deserializationContext) { - JsonToken token = parser.currentToken(); + public UUID deserialize(JsonParser parser, DeserializationContext ctxt) { + var token = parser.currentToken(); if (token == JsonToken.VALUE_STRING) { - String string = parser.getString().trim(); + var value = parser.getString().trim(); if (useFriendlyFormat) { - if (looksLikeUuid(string)) { - return UUID.fromString(string); - } else { - return FriendlyId.toUuid(string); - } + return parseAsUuidOrFriendlyId(value); } else { - return UUID.fromString(string); + return UUID.fromString(value); } } - throw new IllegalStateException("This is not friendly id"); + throw ctxt.weirdStringException(parser.getString(), UUID.class, "Expected UUID string value"); } - private boolean looksLikeUuid(String value) { - return value.contains("-"); + /** + * 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) == '-'; } @Override public ValueDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { if (property != null) { - IdFormat annotation = property.getAnnotation(IdFormat.class); + var annotation = property.getAnnotation(IdFormat.class); if (annotation != null && annotation.value() == FriendlyIdFormat.RAW) { return new FriendlyIdDeserializer(false); } 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..d05f6f7 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 @@ -5,25 +5,7 @@ import com.devskiller.friendly_id.jackson.FriendlyIdFormat; import com.devskiller.friendly_id.jackson.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 c678910..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 @@ -15,53 +15,50 @@ class FieldWithoutFriendlyIdTest { @Test void shouldAllowToDoNotCodeUuidInDataObject() { - Foo foo = new Foo(); - foo.setRawUuid(uuid); - foo.setFriendlyId(uuid); + var foo = new Foo(uuid, uuid); - String json = jsonMapper.writeValueAsString(foo); + var json = jsonMapper.writeValueAsString(foo); // 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 = jsonMapper.readValue(json, Foo.class); - assertThat(cloned.getRawUuid()).isEqualTo(foo.getFriendlyId()); + var cloned = jsonMapper.readValue(json, Foo.class); + assertThat(cloned.rawUuid()).isEqualTo(foo.friendlyId()); } @Test void shouldDeserializeUuidsInDataObject() { - String json = "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}"; + var json = """ + {"rawUuid":"f088ce5b-9279-4cc3-946a-c15ad740dd6d","friendlyId":"7Jsg6CPDscHawyJfE70b9x"}"""; - Foo cloned = jsonMapper.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 void shouldSerializeUuidsInValueObject() { jsonMapper = mapper(); - Bar bar = new Bar(uuid, uuid); + var bar = new Bar(uuid, uuid); - String json = jsonMapper.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 - void shouldDeserializeUuuidsValueObject() { + 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 = jsonMapper.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..082b185 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 @@ -5,26 +5,7 @@ import com.devskiller.friendly_id.jackson.FriendlyIdFormat; import com.devskiller.friendly_id.jackson.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-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 index b973b38..fc8c27b 100644 --- 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 @@ -39,11 +39,14 @@ public static void main(String[] args) { @Bean public CommandLineRunner initData(ProductRepository repository) { return args -> { - System.out.println("\n========================================"); - System.out.println("Initializing demo products..."); - System.out.println("========================================\n"); + System.out.println(""" - Product laptop = new Product( + ======================================== + Initializing demo products... + ======================================== + """); + + var laptop = new Product( "Laptop", "High-performance laptop for developers", new BigDecimal("1299.99"), @@ -52,7 +55,7 @@ public CommandLineRunner initData(ProductRepository repository) { repository.save(laptop); System.out.println("Created product: Laptop with ID: " + laptop.getId()); - Product mouse = new Product( + var mouse = new Product( "Wireless Mouse", "Ergonomic wireless mouse", new BigDecimal("29.99"), @@ -61,7 +64,7 @@ public CommandLineRunner initData(ProductRepository repository) { repository.save(mouse); System.out.println("Created product: Wireless Mouse with ID: " + mouse.getId()); - Product keyboard = new Product( + var keyboard = new Product( "Mechanical Keyboard", "RGB mechanical keyboard with Cherry MX switches", new BigDecimal("149.99"), @@ -70,20 +73,23 @@ public CommandLineRunner initData(ProductRepository repository) { repository.save(keyboard); System.out.println("Created product: Mechanical Keyboard with ID: " + keyboard.getId()); - System.out.println("\n========================================"); - System.out.println("Demo ready!"); - System.out.println("========================================"); - System.out.println("REST API: http://localhost:8080/api/products"); - System.out.println("H2 Console: http://localhost:8080/h2-console"); - System.out.println(" JDBC URL: jdbc:h2:mem:friendlyid_demo"); - System.out.println(" Username: sa"); - System.out.println(" Password: (empty)"); - System.out.println("========================================\n"); + 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 - System.out.println("Try these commands:"); - System.out.println(" curl http://localhost:8080/api/products"); - System.out.println(" curl http://localhost:8080/api/products/" + laptop.getId()); - System.out.println("\n"); + """.formatted(laptop.getId())); }; } } 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..c757e0d 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 @@ -18,9 +18,6 @@ public static void main(String[] args) { @GetMapping("/bars/{id}") Bar getBar(@PathVariable UUID id) { - Bar bar = new Bar(); - bar.setId(id); - return bar; + return new Bar(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 index 5b1ecb7..c697f5f 100644 --- 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 @@ -2,22 +2,4 @@ import java.util.UUID; -public class Bar { - - private UUID id; - - public Bar() { - } - - public Bar(UUID id) { - this.id = id; - } - - public UUID getId() { - return id; - } - - public void setId(UUID id) { - this.id = id; - } -} +public record Bar(UUID id) {} 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 6e0828f..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 @@ -6,13 +6,14 @@ 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. *

    @@ -30,74 +31,16 @@ public class FriendlyIdConfiguration implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { - registry.addConverter(new StringToUuidConverter()); - registry.addConverter(new UuidToStringConverter()); - registry.addConverter(new StringToFriendlyIdConverter()); - registry.addConverter(new FriendlyIdToStringConverter()); + 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 JacksonModule friendlyIdModule() { return new FriendlyIdModule(); } - - /** - * Converter that converts FriendlyId strings to UUID. - *

    - * This converter is automatically registered in Spring's conversion service - * and allows path variables and request parameters to be automatically converted - * from FriendlyId format to UUID. - */ - public static class StringToUuidConverter implements Converter { - - @Override - public UUID convert(String id) { - return FriendlyId.toUuid(id); - } - } - - /** - * Converter that converts UUID to FriendlyId strings. - *

    - * This converter is automatically registered in Spring's conversion service - * and allows UUIDs to be automatically converted to FriendlyId format - * in responses and URL generation. - */ - public static class UuidToStringConverter implements Converter { - - @Override - public String convert(UUID id) { - return FriendlyId.toFriendlyId(id); - } - } - - /** - * Converter that converts FriendlyId strings to FriendlyId value objects. - *

    - * This converter is automatically registered in Spring's conversion service - * and allows path variables and request parameters to be automatically converted - * from FriendlyId format to FriendlyId value object. - */ - public static class StringToFriendlyIdConverter implements Converter { - - @Override - public com.devskiller.friendly_id.type.FriendlyId convert(String id) { - return com.devskiller.friendly_id.type.FriendlyId.fromString(id); - } - } - - /** - * Converter that converts FriendlyId value objects to FriendlyId strings. - *

    - * This converter is automatically registered in Spring's conversion service - * and allows FriendlyId value objects to be automatically converted to FriendlyId format - * in responses and URL generation. - */ - public static class FriendlyIdToStringConverter implements Converter { - - @Override - public String convert(com.devskiller.friendly_id.type.FriendlyId id) { - return id.toString(); - } - } } 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 index f186a55..9fd70d3 100644 --- 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 @@ -120,10 +120,7 @@ public String toString() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - FriendlyId that = (FriendlyId) o; - return uuid.equals(that.uuid); + return this == o || (o instanceof FriendlyId that && uuid.equals(that.uuid)); } @Override From f5acdf48a0928bcbf3195f451bd0adb83c1fe0d3 Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sun, 21 Dec 2025 13:36:44 +0100 Subject: [PATCH 21/27] fix: configure Lombok annotation processor for Maven 3.14+ --- .../friendly-id-spring-boot-customized/pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) 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 cc27f9d..2d65f10 100644 --- a/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml @@ -63,6 +63,13 @@ 3.14.0 true + + + org.projectlombok + lombok + ${lombok.version} + + From f102ef9f01d1d4c4477c8b763d0cba51525f231c Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sun, 21 Dec 2025 14:29:56 +0100 Subject: [PATCH 22/27] fix: configure Lombok and modernize Jackson2 module --- .../java/document_symbols_cache_v23-06-25.pkl | Bin 0 -> 6069 bytes .../FriendlyIdAnnotationIntrospector.java | 32 +++++++----------- .../pom.xml | 21 ++++++++---- .../friendly-id-spring-boot-hateos/pom.xml | 26 ++++++++++---- 4 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 .serena/cache/java/document_symbols_cache_v23-06-25.pkl 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 0000000000000000000000000000000000000000..c21ae4889c094daf887ea05cac9aee84e6d38c45 GIT binary patch literal 6069 zcmd5=TW=dh6i$ilI#q6pY7z=*E{ecSvDZ#~slyX$Wg1x&Xq(5V8t+a#ldN~Gy;uYZ ziAEt%7#@(Os(3`aAt9a-e~xq3yKArORz)ai@-*M%%$&3HeP`wzf0KIt`|?!q_qEK6 z<0FSt%P>z1+$gv`hg+RO+qON;b#$#uxTPJFKGAf$s~NQKo^aEojutI?!VPV(NNa}s z>>GD&>xg%Jhj`q!inzScB&JKHSg+U0yQEZZ@3xH^rHzWd)2@}OHCnE;81^W$fTO7JAj<_uj+)9x;;g%s=DUo)F)u9rXr(KUYUNB6V+@i8IC1!L+9HM*F z0pyEm_-Hg>v#lvM*V^`dG4E27>KOFHXeV}UXfoxq`60pu8H0L+n^H_kku_~S_~5|n zwC`{!o*i+M?rECF>@J1Fbhz(+rFFO$pC26h==jnIkkRR|88J)Ru5J1r{hWAAiZd0W zn{}uOr`Hc-G9sccQbMW+itn?<5sUX=ZZ6gjqAjqv&T@$mQ2O9_oft;bcDlp^vAX$q_>gwE>p7>lFoH}X zf}4TQNS|AP-WrQwEfE2t8yJDOy1uvGx49wN2HRwBV+1QI376eqMfR=|DrKwerq9+6 zWhjuPNTvJ6r?%mnG(Ulv_Y=(An!wBlFt-qE1JPQ*%nUFy*jHh0K2nsM+#?U4vd8v; z&+$R(QwMMUms0n3f}T4Q=-Gj}TuculxcV`5+4f=ZfTYEOK5&p9IGvz^O0aR1pLIaf zu*v8RQH8nr*bSmwpaRU#gM zuf!Vt1km^^^KqctNau)NN|w1OSy0szQsr*2ZL~!b(0HocgSo{x3`EO8s^En=_y$at zCu%0ku}b_0kD0>BLR2~|pQ@>-EtL+5Z9MSVhpYi_)n|}`j2=QknPQvtLS9J0qlyXZ z`eUM>xVBX=h)2BB9)0tUGh8{+at`XGv0k&JS1YA*iI94=Ql|}Ct>4*c(28DdRCe0s zBdGaDd(F+)^qQf=|K4mqgJ$!G*6mj`o8MveJurcG;>RJv1yQpJg-2MWfB&!B%S$?P z2}Sw^sPO5Vx006@$*R!*2a(C34?wXaNoQhl*egaQ`#Cg;-{OBmRx?2Skku7H*d+cm zW&urN$UEAvg2m61n#40P%Pr5b-5&g4VMASe-pVGtWrO{ry%5#Myy4t->MoPtdYFftDQ14U-a4Kuh9x6E)N2Z`J6D>nEiiu3uC-Caj)* SWq+{0g1YG`dlA%4e(@i849pM! literal 0 HcmV?d00001 diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java index 3f82115..331f536 100644 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdAnnotationIntrospector.java @@ -17,12 +17,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 +30,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-samples/friendly-id-spring-boot-customized/pom.xml b/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml index 2d65f10..3fd99ae 100644 --- a/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-customized/pom.xml @@ -37,7 +37,7 @@ org.projectlombok lombok - provided + true @@ -55,23 +55,30 @@ - org.springframework.boot - spring-boot-maven-plugin - - + org.apache.maven.plugins maven-compiler-plugin - 3.14.0 true org.projectlombok lombok - ${lombok.version} + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + org.apache.maven.plugins maven-surefire-plugin 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 04c884a..14bd415 100644 --- a/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml +++ b/friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml @@ -45,7 +45,7 @@ org.projectlombok lombok - provided + true @@ -63,14 +63,28 @@ - org.springframework.boot - spring-boot-maven-plugin - - + org.apache.maven.plugins maven-compiler-plugin - 3.14.0 true + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + From 3dffccbee818a9975797c2c41f28372f89f4c228 Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sun, 21 Dec 2025 14:33:58 +0100 Subject: [PATCH 23/27] chore: update Maven wrapper to 3.9.12 --- .mvn/wrapper/maven-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index c0bcafe..8dea6c2 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,3 +1,3 @@ wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip From 644ec72c03d8708c40d3ee72345effe65e5662d1 Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sun, 21 Dec 2025 14:40:59 +0100 Subject: [PATCH 24/27] refactor: replace manual controller tests with GetBar contract --- .../sample/contracts/BarControllerTest.java | 38 ------------ .../sample/contracts/FooControllerTest.java | 59 ------------------- .../friendly_id/sample/contracts/MvcTest.java | 50 ---------------- .../test/resources/contracts/GetBar.groovy | 26 ++++++++ 4 files changed, 26 insertions(+), 147 deletions(-) delete mode 100644 friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/BarControllerTest.java delete mode 100644 friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/FooControllerTest.java delete mode 100644 friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/MvcTest.java create mode 100644 friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetBar.groovy 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 a67d106..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/BarControllerTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -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; -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; - -@WebMvcTest(BarController.class) -@EnableFriendlyId -@Import(BarResourceAssembler.class) -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/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 4881f13..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/FooControllerTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -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; -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; - -@WebMvcTest(FooController.class) -@EnableFriendlyId -@Import(FooResourceAssembler.class) -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 2f5942f..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/MvcTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import com.devskiller.friendly_id.FriendlyId; -import com.devskiller.friendly_id.jackson.FriendlyIdModule; -import io.restassured.module.mockmvc.RestAssuredMockMvc; -import org.junit.jupiter.api.BeforeEach; -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.JacksonJsonHttpMessageConverter; -import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; -import tools.jackson.databind.json.JsonMapper; - -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; - - @BeforeEach - public void setup() { - mockMvcBuilder = standaloneSetup(new FooController(mock(EntityLinks.class))); - DefaultFormattingConversionService service = new DefaultFormattingConversionService(); - service.addConverter(new StringToUuidConverter()); - mockMvcBuilder.setMessageConverters(jacksonHttpMessageConverter()).setConversionService(service); - RestAssuredMockMvc.standaloneSetup(mockMvcBuilder); - } - - public static class StringToUuidConverter implements Converter { - - @Override - public UUID convert(String id) { - return FriendlyId.toUuid(id); - } - } - - private JacksonJsonHttpMessageConverter jacksonHttpMessageConverter() { - JsonMapper mapper = jsonMapper(); - return new JacksonJsonHttpMessageConverter(mapper); - } - - protected JsonMapper jsonMapper() { - return JsonMapper.builder() - .addModule(new FriendlyIdModule()) - .build(); - } -} diff --git a/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetBar.groovy b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetBar.groovy new file mode 100644 index 0000000..b60eabb --- /dev/null +++ b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetBar.groovy @@ -0,0 +1,26 @@ +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url '/foos/caffe/bars/latte' + headers { + applicationJsonUtf8() + } + } + response { + status 200 + body( + name: 'Bar', + _links: [ + self: [ + href: 'http://localhost/foos/caffe/bars/latte' + ], + foos: [ + href: 'http://localhost/foos' + ] + ] + ) + headers { + applicationJsonUtf8() + } + } +} From 9a6285fc1d6071921fecbd9b915255f85751dfe4 Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sun, 21 Dec 2025 14:47:11 +0100 Subject: [PATCH 25/27] refactor: rename Jackson 2.x package to jackson2 to avoid conflicts --- .../FriendlyIdAnnotationIntrospector.java | 2 +- .../{jackson => jackson2}/FriendlyIdDeserializer.java | 2 +- .../friendly_id/{jackson => jackson2}/FriendlyIdFormat.java | 2 +- .../friendly_id/{jackson => jackson2}/FriendlyIdModule.java | 2 +- .../{jackson => jackson2}/FriendlyIdSerializer.java | 2 +- .../{jackson => jackson2}/FriendlyIdValueDeserializer.java | 2 +- .../{jackson => jackson2}/FriendlyIdValueSerializer.java | 2 +- .../friendly_id/{jackson => jackson2}/IdFormat.java | 2 +- .../META-INF/services/com.fasterxml.jackson.databind.Module | 2 +- .../src/test/java/com/devskiller/friendly_id/spring/Bar.java | 4 ++-- .../src/test/java/com/devskiller/friendly_id/spring/Foo.java | 4 ++-- .../friendly_id/spring/ObjectMapperConfiguration.java | 2 +- .../friendly_id/sample/spring3/FriendlyIdConfig.java | 2 +- 13 files changed, 15 insertions(+), 15 deletions(-) rename friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/{jackson => jackson2}/FriendlyIdAnnotationIntrospector.java (97%) rename friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/{jackson => jackson2}/FriendlyIdDeserializer.java (95%) rename friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/{jackson => jackson2}/FriendlyIdFormat.java (78%) rename friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/{jackson => jackson2}/FriendlyIdModule.java (94%) rename friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/{jackson => jackson2}/FriendlyIdSerializer.java (92%) rename friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/{jackson => jackson2}/FriendlyIdValueDeserializer.java (93%) rename friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/{jackson => jackson2}/FriendlyIdValueSerializer.java (93%) rename friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/{jackson => jackson2}/IdFormat.java (86%) diff --git a/friendly-id-jackson2-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 97% rename from friendly-id-jackson2-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 331f536..61d3c90 100644 --- a/friendly-id-jackson2-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,4 +1,4 @@ -package com.devskiller.friendly_id.jackson; +package com.devskiller.friendly_id.jackson2; import java.util.UUID; diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdDeserializer.java similarity index 95% rename from friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java rename to friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdDeserializer.java index 3e0d89a..e013098 100644 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdDeserializer.java @@ -1,4 +1,4 @@ -package com.devskiller.friendly_id.jackson; +package com.devskiller.friendly_id.jackson2; import java.io.IOException; import java.util.UUID; diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdFormat.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdFormat.java similarity index 78% rename from friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdFormat.java rename to friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdFormat.java index 2fb1985..5851bf5 100644 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdFormat.java +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdFormat.java @@ -1,4 +1,4 @@ -package com.devskiller.friendly_id.jackson; +package com.devskiller.friendly_id.jackson2; /** * Friendly ID format diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdModule.java similarity index 94% rename from friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java rename to friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdModule.java index 418a1f2..11d49ff 100644 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdModule.java @@ -1,4 +1,4 @@ -package com.devskiller.friendly_id.jackson; +package com.devskiller.friendly_id.jackson2; import java.util.UUID; diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdSerializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdSerializer.java similarity index 92% rename from friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdSerializer.java rename to friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdSerializer.java index f80b803..87b3012 100644 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdSerializer.java +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdSerializer.java @@ -1,4 +1,4 @@ -package com.devskiller.friendly_id.jackson; +package com.devskiller.friendly_id.jackson2; import java.io.IOException; import java.util.UUID; diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueDeserializer.java similarity index 93% rename from friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java rename to friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueDeserializer.java index 569d0d1..d352651 100644 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueDeserializer.java @@ -1,4 +1,4 @@ -package com.devskiller.friendly_id.jackson; +package com.devskiller.friendly_id.jackson2; import java.io.IOException; diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueSerializer.java similarity index 93% rename from friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java rename to friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueSerializer.java index 627ed51..0f764cf 100644 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueSerializer.java @@ -1,4 +1,4 @@ -package com.devskiller.friendly_id.jackson; +package com.devskiller.friendly_id.jackson2; import java.io.IOException; diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/IdFormat.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/IdFormat.java similarity index 86% rename from friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/IdFormat.java rename to friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/IdFormat.java index 999c366..d65acb2 100644 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson/IdFormat.java +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/IdFormat.java @@ -1,4 +1,4 @@ -package com.devskiller.friendly_id.jackson; +package com.devskiller.friendly_id.jackson2; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; 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 index 781a57d..55ab5c7 100644 --- 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 @@ -1 +1 @@ -com.devskiller.friendly_id.jackson.FriendlyIdModule +com.devskiller.friendly_id.jackson2.FriendlyIdModule 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 index ba9fb63..7cf8372 100644 --- 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 @@ -2,8 +2,8 @@ import java.util.UUID; -import com.devskiller.friendly_id.jackson.FriendlyIdFormat; -import com.devskiller.friendly_id.jackson.IdFormat; +import com.devskiller.friendly_id.jackson2.FriendlyIdFormat; +import com.devskiller.friendly_id.jackson2.IdFormat; public class Bar { 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 index 8c96109..7824919 100644 --- 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 @@ -2,8 +2,8 @@ import java.util.UUID; -import com.devskiller.friendly_id.jackson.FriendlyIdFormat; -import com.devskiller.friendly_id.jackson.IdFormat; +import com.devskiller.friendly_id.jackson2.FriendlyIdFormat; +import com.devskiller.friendly_id.jackson2.IdFormat; public class Foo { 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 index e200068..89971e7 100644 --- 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 @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; -import com.devskiller.friendly_id.jackson.FriendlyIdModule; +import com.devskiller.friendly_id.jackson2.FriendlyIdModule; public class ObjectMapperConfiguration { 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 index 5e347e0..71c4ec1 100644 --- 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 @@ -9,7 +9,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.devskiller.friendly_id.FriendlyId; -import com.devskiller.friendly_id.jackson.FriendlyIdModule; +import com.devskiller.friendly_id.jackson2.FriendlyIdModule; @Configuration public class FriendlyIdConfig implements WebMvcConfigurer { From de2898d7250ebcba6aa28869511d5ee287d7fa38 Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sun, 21 Dec 2025 15:03:00 +0100 Subject: [PATCH 26/27] refactor: move IdFormat annotation to core module - Rename FriendlyIdModule to FriendlyIdJackson2Module in jackson2 module - Move IdFormat and FriendlyIdFormat to friendly-id core module - Update all imports in Jackson modules and samples - Fix Lombok configuration in contracts sample --- .../jackson/FriendlyIdDeserializer.java | 2 ++ .../jackson/FriendlyIdSerializer.java | 2 ++ .../com/devskiller/friendly_id/spring/Bar.java | 4 ++-- .../com/devskiller/friendly_id/spring/Foo.java | 4 ++-- .../FriendlyIdAnnotationIntrospector.java | 2 ++ .../friendly_id/jackson2/FriendlyIdFormat.java | 17 ----------------- ...odule.java => FriendlyIdJackson2Module.java} | 11 ++++++----- .../friendly_id/jackson2/IdFormat.java | 14 -------------- .../com.fasterxml.jackson.databind.Module | 2 +- .../com/devskiller/friendly_id/spring/Bar.java | 4 ++-- .../com/devskiller/friendly_id/spring/Foo.java | 4 ++-- .../spring/ObjectMapperConfiguration.java | 4 ++-- .../friendly-id-contracts/pom.xml | 16 +++++++++++++++- .../friendly_id/sample/customized/Bar.java | 4 ++-- .../sample/spring3/FriendlyIdConfig.java | 6 +++--- .../friendly_id}/FriendlyIdFormat.java | 2 +- .../com/devskiller/friendly_id}/IdFormat.java | 2 +- 17 files changed, 45 insertions(+), 55 deletions(-) delete mode 100644 friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdFormat.java rename friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/{FriendlyIdModule.java => FriendlyIdJackson2Module.java} (59%) delete mode 100644 friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/IdFormat.java rename {friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson => friendly-id/src/main/java/com/devskiller/friendly_id}/FriendlyIdFormat.java (78%) rename {friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson => friendly-id/src/main/java/com/devskiller/friendly_id}/IdFormat.java (86%) 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 b3f71d4..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 @@ -10,6 +10,8 @@ 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 StdDeserializer { 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 c5b1c83..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 @@ -9,6 +9,8 @@ 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 { 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 d05f6f7..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,8 +2,8 @@ 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 record Bar( @IdFormat(FriendlyIdFormat.RAW) UUID rawUuid, 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 082b185..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,8 +2,8 @@ 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 record Foo( @IdFormat(FriendlyIdFormat.RAW) UUID rawUuid, diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdAnnotationIntrospector.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdAnnotationIntrospector.java index 61d3c90..3da47d3 100644 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdAnnotationIntrospector.java +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdAnnotationIntrospector.java @@ -2,6 +2,8 @@ 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; diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdFormat.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdFormat.java deleted file mode 100644 index 5851bf5..0000000 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdFormat.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.devskiller.friendly_id.jackson2; - -/** - * Friendly ID format - */ -public enum FriendlyIdFormat { - - /** - * Url62 encoded ID - */ - URL62, - - /** - * Leave this ID as is (without conversion) - */ - RAW -} diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdModule.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdJackson2Module.java similarity index 59% rename from friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdModule.java rename to friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdJackson2Module.java index 11d49ff..3e9fc3b 100644 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdModule.java +++ b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdJackson2Module.java @@ -2,20 +2,21 @@ import java.util.UUID; +import com.devskiller.friendly_id.type.FriendlyId; import com.fasterxml.jackson.databind.module.SimpleModule; -public class FriendlyIdModule extends SimpleModule { +public class FriendlyIdJackson2Module extends SimpleModule { - private FriendlyIdAnnotationIntrospector introspector; + private final FriendlyIdAnnotationIntrospector introspector; - public FriendlyIdModule() { + public FriendlyIdJackson2Module() { introspector = new FriendlyIdAnnotationIntrospector(); addDeserializer(UUID.class, new FriendlyIdDeserializer()); addSerializer(UUID.class, new FriendlyIdSerializer()); // Add serializer/deserializer for FriendlyId value object - addDeserializer(com.devskiller.friendly_id.type.FriendlyId.class, new FriendlyIdValueDeserializer()); - addSerializer(com.devskiller.friendly_id.type.FriendlyId.class, new FriendlyIdValueSerializer()); + addDeserializer(FriendlyId.class, new FriendlyIdValueDeserializer()); + addSerializer(FriendlyId.class, new FriendlyIdValueSerializer()); } @Override diff --git a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/IdFormat.java b/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/IdFormat.java deleted file mode 100644 index d65acb2..0000000 --- a/friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/IdFormat.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.devskiller.friendly_id.jackson2; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * Declares that a field should be formatted as a friendly ID. - */ -@Retention(RetentionPolicy.RUNTIME) -public @interface IdFormat { - - FriendlyIdFormat value() default FriendlyIdFormat.URL62; - -} 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 index 55ab5c7..de36ea4 100644 --- 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 @@ -1 +1 @@ -com.devskiller.friendly_id.jackson2.FriendlyIdModule +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 index 7cf8372..6d87496 100644 --- 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 @@ -2,8 +2,8 @@ import java.util.UUID; -import com.devskiller.friendly_id.jackson2.FriendlyIdFormat; -import com.devskiller.friendly_id.jackson2.IdFormat; +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; public class Bar { 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 index 7824919..c866ed3 100644 --- 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 @@ -2,8 +2,8 @@ import java.util.UUID; -import com.devskiller.friendly_id.jackson2.FriendlyIdFormat; -import com.devskiller.friendly_id.jackson2.IdFormat; +import com.devskiller.friendly_id.FriendlyIdFormat; +import com.devskiller.friendly_id.IdFormat; public class Foo { 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 index 89971e7..6149e61 100644 --- 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 @@ -3,13 +3,13 @@ import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; -import com.devskiller.friendly_id.jackson2.FriendlyIdModule; +import com.devskiller.friendly_id.jackson2.FriendlyIdJackson2Module; public class ObjectMapperConfiguration { protected static ObjectMapper mapper(Module... modules) { ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new FriendlyIdModule()); + mapper.registerModule(new FriendlyIdJackson2Module()); mapper.registerModules(modules); return mapper; } diff --git a/friendly-id-samples/friendly-id-contracts/pom.xml b/friendly-id-samples/friendly-id-contracts/pom.xml index 26ae10b..ced6f91 100644 --- a/friendly-id-samples/friendly-id-contracts/pom.xml +++ b/friendly-id-samples/friendly-id-contracts/pom.xml @@ -51,7 +51,7 @@ org.projectlombok lombok - provided + true @@ -83,12 +83,26 @@ org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + maven-compiler-plugin 3.14.0 true + + + org.projectlombok + lombok + + 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 index 7b5fd41..8e4d610 100644 --- 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 @@ -2,9 +2,9 @@ import java.util.UUID; -import com.devskiller.friendly_id.jackson.IdFormat; +import com.devskiller.friendly_id.IdFormat; -import static com.devskiller.friendly_id.jackson.FriendlyIdFormat.RAW; +import static com.devskiller.friendly_id.FriendlyIdFormat.RAW; record Bar(UUID friendlyId, @IdFormat(RAW) UUID uuid) { } 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 index 71c4ec1..69d28e3 100644 --- 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 @@ -9,7 +9,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.devskiller.friendly_id.FriendlyId; -import com.devskiller.friendly_id.jackson2.FriendlyIdModule; +import com.devskiller.friendly_id.jackson2.FriendlyIdJackson2Module; @Configuration public class FriendlyIdConfig implements WebMvcConfigurer { @@ -21,8 +21,8 @@ public void addFormatters(FormatterRegistry registry) { } @Bean - FriendlyIdModule friendlyIdModule() { - return new FriendlyIdModule(); + FriendlyIdJackson2Module friendlyIdModule() { + return new FriendlyIdJackson2Module(); } public static class StringToUuidConverter implements Converter { 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; From f92d9ffd8237750830af4ee6cb00716ba5cefb5b Mon Sep 17 00:00:00 2001 From: mariuszs Date: Sun, 21 Dec 2025 17:11:22 +0100 Subject: [PATCH 27/27] refactor: simplify samples and add security contracts - Replace Bar/Foo with Item record in all samples - Remove HATEOAS from contracts sample - Add Spring Security with authenticated/unauthenticated contracts - Add tests for @PathVariable and @RequestParam with UUID and FriendlyId types - Update contract files with proper package declarations --- README.md | 12 +- .../friendly-id-contracts/pom.xml | 31 +++-- .../sample/contracts/AdminController.java | 15 +++ .../sample/contracts/BarController.java | 29 ----- .../sample/contracts/BarResource.java | 19 --- .../contracts/BarResourceAssembler.java | 30 ----- .../sample/contracts/FooController.java | 58 --------- .../sample/contracts/FooResource.java | 22 ---- .../contracts/FooResourceAssembler.java | 28 ---- .../friendly_id/sample/contracts/Item.java | 23 ++++ .../sample/contracts/ItemController.java | 33 +++++ .../sample/contracts/SecurityConfig.java | 25 ++++ .../sample/contracts/domain/Bar.java | 6 - .../sample/contracts/domain/Foo.java | 6 - .../contracts/AuthenticatedContractBase.java | 30 +++++ .../contracts/ContractVerifierBase.java | 6 +- .../contracts/AdminUnauthorized.groovy | 16 +++ .../contracts/CreateItemUnauthorized.groovy | 22 ++++ .../test/resources/contracts/GetBar.groovy | 26 ---- .../test/resources/contracts/GetFoo.groovy | 25 ---- .../test/resources/contracts/GetItem.groovy | 25 ++++ .../authenticated/AdminAuthorized.groovy | 17 +++ .../contracts/authenticated/CreateItem.groovy | 31 +++++ .../friendly_id/sample/customized/Bar.java | 10 -- .../sample/customized/FooService.java | 21 --- .../friendly_id/sample/customized/Item.java | 23 ++++ ...BarController.java => ItemController.java} | 25 ++-- .../sample/customized/ItemService.java | 31 +++++ .../sample/customized/ApplicationTest.java | 86 +++++++++---- .../sample/simple/Application.java | 31 ++++- .../friendly_id/sample/simple/Bar.java | 5 - .../friendly_id/sample/simple/Item.java | 23 ++++ .../sample/simple/ApplicationTest.java | 121 +++++++++++++++++- .../sample/spring3/Application.java | 21 ++- .../friendly_id/sample/spring3/Bar.java | 23 ---- .../friendly_id/sample/spring3/Item.java | 23 ++++ .../sample/spring3/ApplicationTest.java | 47 ++++++- 37 files changed, 650 insertions(+), 375 deletions(-) create mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/AdminController.java delete mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarController.java delete mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarResource.java delete mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarResourceAssembler.java delete mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooController.java delete mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooResource.java delete mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooResourceAssembler.java create mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/Item.java create mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/ItemController.java create mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/SecurityConfig.java delete mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/domain/Bar.java delete mode 100644 friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/domain/Foo.java create mode 100644 friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/AuthenticatedContractBase.java create mode 100644 friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/AdminUnauthorized.groovy create mode 100644 friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/CreateItemUnauthorized.groovy delete mode 100644 friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetBar.groovy delete mode 100644 friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetFoo.groovy create mode 100644 friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetItem.groovy create mode 100644 friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/authenticated/AdminAuthorized.groovy create mode 100644 friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/authenticated/CreateItem.groovy delete mode 100644 friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/Bar.java delete mode 100644 friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/FooService.java create mode 100644 friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/Item.java rename friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/{BarController.java => ItemController.java} (51%) create mode 100644 friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/ItemService.java delete mode 100644 friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Bar.java create mode 100644 friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Item.java delete mode 100644 friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Bar.java create mode 100644 friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Item.java diff --git a/README.md b/README.md index 8eed306..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 ``` @@ -161,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 ``` @@ -205,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: @@ -224,7 +224,7 @@ First, add the dependency: com.devskiller.friendly-id friendly-id-jooq - 1.1.0 + 2.0.0-alpha6 ``` @@ -251,7 +251,7 @@ First, add the dependency: com.devskiller.friendly-id friendly-id-jpa - 1.1.0 + 2.0.0-alpha6 ``` @@ -279,7 +279,7 @@ First, add the dependency: com.devskiller.friendly-id friendly-id-openfeign - 1.1.0 + 2.0.0-alpha6 ``` diff --git a/friendly-id-samples/friendly-id-contracts/pom.xml b/friendly-id-samples/friendly-id-contracts/pom.xml index ced6f91..4e6eccb 100644 --- a/friendly-id-samples/friendly-id-contracts/pom.xml +++ b/friendly-id-samples/friendly-id-contracts/pom.xml @@ -20,6 +20,7 @@ UTF-8 21 5.0.1 + 6.0.0 @@ -27,25 +28,31 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-test + test + org.springframework.cloud spring-cloud-starter-contract-verifier test + + io.rest-assured + spring-mock-mvc + ${rest-assured.version} + test + com.devskiller.friendly-id friendly-id-spring-boot-starter ${project.version} - - org.springframework.boot - spring-boot-starter-hateoas - - - org.atteo - evo-inflector - 1.2.2 - @@ -124,6 +131,12 @@ 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 805f415..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarResource.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -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") -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -@AllArgsConstructor -class BarResource extends RepresentationModel { - - 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 66b8e93..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/BarResourceAssembler.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import com.devskiller.friendly_id.sample.contracts.domain.Bar; -import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; -import org.springframework.stereotype.Component; - -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() { - super(BarController.class, BarResource.class); - } - - @Override - public BarResource toModel(Bar entity) { - 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-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 1b3eb0b..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooController.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import com.devskiller.friendly_id.sample.contracts.domain.Foo; -import lombok.extern.slf4j.Slf4j; -import org.springframework.hateoas.server.EntityLinks; -import org.springframework.hateoas.server.ExposesResourceFor; -import org.springframework.http.HttpEntity; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.net.URI; -import java.util.UUID; - -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 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) { - log.info("Create {}", fooResource.getUuid()); - - // Modern Spring HATEOAS 2.x - methodOn() triggers automatic FriendlyId conversion - URI location = linkTo(methodOn(FooController.class) - .get(fooResource.getUuid())) - .toUri(); - - return ResponseEntity.created(location).build(); - } - -} 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 4746394..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooResource.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.server.core.Relation; - -import java.util.UUID; - -@Relation(value = "foos") -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -@AllArgsConstructor -public class FooResource extends RepresentationModel { - - UUID uuid; - 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 26e0c27..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/FooResourceAssembler.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts; - -import com.devskiller.friendly_id.sample.contracts.domain.Foo; -import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; -import org.springframework.stereotype.Component; - -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() { - super(FooController.class, FooResource.class); - } - - @Override - public FooResource toModel(Foo entity) { - FooResource resource = new FooResource(entity.id(), entity.name()); - - // Modern Spring HATEOAS 2.x - methodOn() triggers automatic FriendlyId conversion - resource.add(linkTo(methodOn(FooController.class) - .get(entity.id())) - .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/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 78880ac..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/domain/Bar.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts.domain; - -import java.util.UUID; - -public record Bar(UUID id, String name, 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 dcff90f..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/domain/Foo.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.devskiller.friendly_id.sample.contracts.domain; - -import java.util.UUID; - -public record Foo(UUID id, 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/ContractVerifierBase.java b/friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/ContractVerifierBase.java index 9d2dde2..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 @@ -10,9 +10,11 @@ import com.devskiller.friendly_id.spring.EnableFriendlyId; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + @WebMvcTest @EnableFriendlyId -@Import({FooResourceAssembler.class, BarResourceAssembler.class}) +@Import(SecurityConfig.class) public abstract class ContractVerifierBase { @Autowired @@ -20,7 +22,7 @@ public abstract class ContractVerifierBase { @BeforeEach public void setUp() { - RestAssuredMockMvc.webAppContextSetup(context); + RestAssuredMockMvc.webAppContextSetup(context, springSecurity()); } } 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/GetBar.groovy b/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetBar.groovy deleted file mode 100644 index b60eabb..0000000 --- a/friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetBar.groovy +++ /dev/null @@ -1,26 +0,0 @@ -org.springframework.cloud.contract.spec.Contract.make { - request { - method 'GET' - url '/foos/caffe/bars/latte' - headers { - applicationJsonUtf8() - } - } - response { - status 200 - body( - name: 'Bar', - _links: [ - self: [ - href: 'http://localhost/foos/caffe/bars/latte' - ], - foos: [ - href: 'http://localhost/foos' - ] - ] - ) - headers { - applicationJsonUtf8() - } - } -} 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/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 8e4d610..0000000 --- a/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/Bar.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.devskiller.friendly_id.sample.customized; - -import java.util.UUID; - -import com.devskiller.friendly_id.IdFormat; - -import static com.devskiller.friendly_id.FriendlyIdFormat.RAW; - -record Bar(UUID friendlyId, @IdFormat(RAW) UUID uuid) { -} 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 a2eab23..0000000 --- a/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/FooService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.devskiller.friendly_id.sample.customized; - -import java.util.UUID; - -import lombok.extern.slf4j.Slf4j; - -import org.springframework.stereotype.Service; - -@Slf4j -@Service -public class FooService { - - public Bar find(UUID uuid) { - log.info("find: {}",uuid); - return new Bar(uuid, uuid); - } - - public void update(UUID id, Bar bar) { - log.info("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/BarController.java b/friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/ItemController.java similarity index 51% rename from friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/BarController.java rename to friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/ItemController.java index e7a3663..9479ef6 100644 --- 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/ItemController.java @@ -6,6 +6,7 @@ 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; @@ -13,23 +14,29 @@ @Slf4j @RestController -@RequestMapping("/bars") -public class BarController { +@RequestMapping("/items") +public class ItemController { - private final FooService fooService; + private final ItemService itemService; - public BarController(FooService fooService) { - this.fooService = fooService; + public ItemController(ItemService itemService) { + this.itemService = itemService; } @GetMapping("/{id}") - public Bar get(@PathVariable UUID id) { + public Item get(@PathVariable UUID id) { log.info("get {}", id); - return fooService.find(id); + return itemService.find(id); + } + + @PostMapping + public Item create(@RequestBody Item item) { + log.info("create {}", item); + return itemService.create(item); } @PutMapping("/{id}") - public void getBar(@PathVariable UUID id, @RequestBody Bar body) { - fooService.update(id, body); + 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 18cbc21..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,7 +1,7 @@ package com.devskiller.friendly_id.sample.customized; -import com.devskiller.friendly_id.FriendlyId; -import com.devskiller.friendly_id.spring.EnableFriendlyId; +import java.util.UUID; + import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; @@ -9,8 +9,10 @@ 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; @@ -18,11 +20,12 @@ 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.*; -@WebMvcTest(BarController.class) +@WebMvcTest(ItemController.class) @EnableFriendlyId class ApplicationTest { @@ -30,57 +33,86 @@ class ApplicationTest { MockMvc mockMvc; @MockitoBean - FooService fooService; + ItemService itemService; @Test - 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)) - .accept(MediaType.APPLICATION_JSON)) + 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 + void shouldDeserializeAndCreate() throws Exception { + // given + UUID uuid = UUID.randomUUID(); + 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(post("/items") + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()); + + // then + then(itemService).should().create(any(Item.class)); } @Test - void shouldDeserialize() throws Exception { + void shouldDeserializeAndUpdate() throws Exception { // given UUID uuid = UUID.randomUUID(); - String json = "{\"friendlyId\":\"" + FriendlyId.toFriendlyId(uuid) + "\",\"uuid\":\"" + uuid + "\"}"; + String friendlyId = toFriendlyId(uuid); + String json = """ + {"id": "%s"} + """.formatted(friendlyId); // when - mockMvc.perform(put("/bars/{id}", FriendlyId.toFriendlyId(uuid)) - .content(json) - .contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform(put("/items/{id}", friendlyId) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()); // then - then(fooService) - .should().update(eq(uuid), any(Bar.class)); + then(itemService).should().update(eq(uuid), any(Item.class)); } @Test - void sampleTestUsingPseudoUuid() throws Exception { + 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") - .accept(MediaType.APPLICATION_JSON)) + 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-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 c757e0d..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,8 +21,28 @@ public static void main(String[] args) { SpringApplication.run(Application.class, args); } - @GetMapping("/bars/{id}") - Bar getBar(@PathVariable UUID id) { - return new Bar(id); + @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 c697f5f..0000000 --- a/friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Bar.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.devskiller.friendly_id.sample.simple; - -import java.util.UUID; - -public record Bar(UUID 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 6cd9638..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 @@ -1,6 +1,7 @@ package com.devskiller.friendly_id.sample.simple; -import com.devskiller.friendly_id.FriendlyId; +import java.util.UUID; + import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -8,10 +9,12 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import java.util.UUID; +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 @@ -22,16 +25,126 @@ class ApplicationTest { private MockMvc mockMvc; @Test - void shouldSerialize() throws Exception { + void shouldAcceptFriendlyIdAsPathVariable() throws Exception { // given UUID uuid = UUID.randomUUID(); String friendlyId = FriendlyId.toFriendlyId(uuid); + String rawUuid = uuid.toString(); // when/then - mockMvc.perform(get("/bars/{id}", friendlyId) + 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 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))); } + + @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())); + } + + @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/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 index d42256a..0c0c88d 100644 --- 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 @@ -6,8 +6,12 @@ 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 { @@ -16,11 +20,18 @@ 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; } } diff --git a/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Bar.java b/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Bar.java deleted file mode 100644 index 9192653..0000000 --- a/friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Bar.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.devskiller.friendly_id.sample.spring3; - -import java.util.UUID; - -public class Bar { - - private UUID id; - - public Bar() { - } - - public Bar(UUID id) { - this.id = id; - } - - public UUID getId() { - return id; - } - - public void setId(UUID id) { - this.id = 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 index cbb30bf..7cfec84 100644 --- 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 @@ -12,7 +12,9 @@ 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 @@ -23,16 +25,57 @@ class ApplicationTest { private MockMvc mockMvc; @Test - void shouldSerialize() throws Exception { + void shouldSerializeAllIdFormats() throws Exception { // given UUID uuid = UUID.randomUUID(); String friendlyId = FriendlyId.toFriendlyId(uuid); + String rawUuid = uuid.toString(); // when/then - mockMvc.perform(get("/bars/{id}", friendlyId) + 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())); + } }