├── .gitignore ├── friendly-id-jackson-datatype ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── services │ │ │ │ └── tools.jackson.databind.JacksonModule │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ └── jackson │ │ │ ├── FriendlyIdValueSerializer.java │ │ │ ├── FriendlyIdValueDeserializer.java │ │ │ ├── FriendlyIdModule.java │ │ │ ├── FriendlyIdSerializer.java │ │ │ └── FriendlyIdDeserializer.java │ └── test │ │ └── java │ │ └── com │ │ └── devskiller │ │ └── friendly_id │ │ └── spring │ │ ├── Bar.java │ │ ├── Foo.java │ │ ├── ObjectMapperConfiguration.java │ │ ├── FriendlyIdDeserializerTest.java │ │ └── FieldWithoutFriendlyIdTest.java └── pom.xml ├── .serena └── cache │ └── java │ └── document_symbols_cache_v23-06-25.pkl ├── friendly-id-jackson2-datatype ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── services │ │ │ │ └── com.fasterxml.jackson.databind.Module │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ └── jackson2 │ │ │ ├── FriendlyIdSerializer.java │ │ │ ├── FriendlyIdValueSerializer.java │ │ │ ├── FriendlyIdValueDeserializer.java │ │ │ ├── FriendlyIdDeserializer.java │ │ │ ├── FriendlyIdJackson2Module.java │ │ │ └── FriendlyIdAnnotationIntrospector.java │ └── test │ │ └── java │ │ └── com │ │ └── devskiller │ │ └── friendly_id │ │ └── spring │ │ ├── ObjectMapperConfiguration.java │ │ ├── Bar.java │ │ ├── Foo.java │ │ ├── FriendlyIdDeserializerTest.java │ │ └── FieldWithoutFriendlyIdTest.java └── pom.xml ├── friendly-id-spring-boot-starter ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── spring │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── java │ │ └── com │ │ └── devskiller │ │ └── friendly_id │ │ └── boot │ │ └── FriendlyIdAutoConfiguration.java └── pom.xml ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── .sdkmanrc ├── friendly-id-samples ├── friendly-id-spring-boot-hateos │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── devskiller │ │ │ │ └── friendly_id │ │ │ │ └── sample │ │ │ │ └── hateos │ │ │ │ ├── domain │ │ │ │ ├── Foo.java │ │ │ │ └── Bar.java │ │ │ │ ├── Application.java │ │ │ │ ├── BarResource.java │ │ │ │ ├── BarController.java │ │ │ │ ├── BarResourceAssembler.java │ │ │ │ ├── FooResource.java │ │ │ │ ├── FooResourceAssembler.java │ │ │ │ └── FooController.java │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ └── sample │ │ │ └── hateos │ │ │ ├── BarControllerTest.java │ │ │ └── FooControllerTest.java │ └── pom.xml ├── friendly-id-spring-boot-jpa-demo │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── devskiller │ │ │ │ │ └── friendly_id │ │ │ │ │ └── sample │ │ │ │ │ └── jpa │ │ │ │ │ ├── ProductRequest.java │ │ │ │ │ ├── ProductRepository.java │ │ │ │ │ ├── Product.java │ │ │ │ │ ├── FriendlyIdJpaDemoApplication.java │ │ │ │ │ └── ProductController.java │ │ │ └── resources │ │ │ │ └── application.properties │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ └── sample │ │ │ └── jpa │ │ │ └── ProductClientIntegrationTest.java │ └── pom.xml ├── friendly-id-contracts │ └── src │ │ ├── test │ │ ├── resources │ │ │ └── contracts │ │ │ │ ├── AdminUnauthorized.groovy │ │ │ │ ├── authenticated │ │ │ │ ├── AdminAuthorized.groovy │ │ │ │ └── CreateItem.groovy │ │ │ │ ├── CreateItemUnauthorized.groovy │ │ │ │ └── GetItem.groovy │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ └── sample │ │ │ └── contracts │ │ │ ├── ContractVerifierBase.java │ │ │ └── AuthenticatedContractBase.java │ │ └── main │ │ └── java │ │ └── com │ │ └── devskiller │ │ └── friendly_id │ │ └── sample │ │ └── contracts │ │ ├── Application.java │ │ ├── AdminController.java │ │ ├── Item.java │ │ ├── SecurityConfig.java │ │ └── ItemController.java ├── friendly-id-spring-boot-customized │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── devskiller │ │ │ │ └── friendly_id │ │ │ │ └── sample │ │ │ │ └── customized │ │ │ │ ├── Application.java │ │ │ │ ├── ItemService.java │ │ │ │ ├── Item.java │ │ │ │ └── ItemController.java │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ └── sample │ │ │ └── customized │ │ │ └── ApplicationTest.java │ └── pom.xml ├── friendly-id-spring-boot-simple │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── devskiller │ │ │ │ └── friendly_id │ │ │ │ └── sample │ │ │ │ └── simple │ │ │ │ ├── Item.java │ │ │ │ └── Application.java │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ └── sample │ │ │ └── simple │ │ │ └── ApplicationTest.java │ └── pom.xml ├── friendly-id-spring-boot3-simple │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── devskiller │ │ │ │ └── friendly_id │ │ │ │ └── sample │ │ │ │ └── spring3 │ │ │ │ ├── Item.java │ │ │ │ ├── Application.java │ │ │ │ └── FriendlyIdConfig.java │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ └── sample │ │ │ └── spring3 │ │ │ └── ApplicationTest.java │ └── pom.xml └── pom.xml ├── .travis.yml ├── friendly-id ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ ├── FriendlyIdFormat.java │ │ │ ├── IdFormat.java │ │ │ ├── package-info.java │ │ │ ├── UuidConverter.java │ │ │ ├── type │ │ │ ├── package-info.java │ │ │ └── FriendlyId.java │ │ │ ├── Url62.java │ │ │ ├── BigIntegerPairing.java │ │ │ ├── FriendlyId.java │ │ │ ├── FriendlyIds.java │ │ │ └── Base62.java │ ├── jmh │ │ ├── resources │ │ │ └── logback-test.xml │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ ├── FriendlyIdBenchmark.java │ │ │ └── UuidConverterBenchmark.java │ └── test │ │ └── java │ │ └── com │ │ └── devskiller │ │ └── friendly_id │ │ ├── IdUtil.java │ │ ├── Url62Test.java │ │ ├── FriendlyIdsTest.java │ │ ├── AnalyzeGeneratedIdsTest.java │ │ ├── BigIntegerPairingTest.java │ │ ├── Base62Test.java │ │ └── type │ │ └── FriendlyIdTest.java └── pom.xml ├── friendly-id-jpa ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ └── jpa │ │ │ ├── package-info.java │ │ │ └── FriendlyIdConverter.java │ └── test │ │ └── java │ │ └── com │ │ └── devskiller │ │ └── friendly_id │ │ └── jpa │ │ └── FriendlyIdConverterTest.java ├── pom.xml └── README.md ├── friendly-id-jooq ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ └── jooq │ │ │ ├── package-info.java │ │ │ └── FriendlyIdConverter.java │ └── test │ │ └── java │ │ └── com │ │ └── devskiller │ │ └── friendly_id │ │ └── jooq │ │ └── FriendlyIdConverterTest.java ├── pom.xml └── README.md ├── .github └── workflows │ ├── maven.yml │ └── release.yml ├── friendly-id-spring-boot ├── src │ └── main │ │ └── java │ │ └── com │ │ └── devskiller │ │ └── friendly_id │ │ └── spring │ │ ├── EnableFriendlyId.java │ │ └── FriendlyIdConfiguration.java └── pom.xml ├── friendly-id-openfeign ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── devskiller │ │ │ └── friendly_id │ │ │ └── openfeign │ │ │ ├── FriendlyIdEncoder.java │ │ │ ├── FriendlyIdDecoder.java │ │ │ └── FriendlyIdConfiguration.java │ └── test │ │ └── java │ │ └── com │ │ └── devskiller │ │ └── friendly_id │ │ └── openfeign │ │ ├── FriendlyIdEncoderTest.java │ │ └── FriendlyIdDecoderTest.java └── pom.xml ├── CHANGELOG.md ├── PUBLISHING.md └── RELEASING.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target 4 | pom.xml.versionsBackup -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/src/main/resources/META-INF/services/tools.jackson.databind.JacksonModule: -------------------------------------------------------------------------------- 1 | com.devskiller.friendly_id.jackson.FriendlyIdModule -------------------------------------------------------------------------------- /.serena/cache/java/document_symbols_cache_v23-06-25.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkillPanel/friendly-id/HEAD/.serena/cache/java/document_symbols_cache_v23-06-25.pkl -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module: -------------------------------------------------------------------------------- 1 | com.devskiller.friendly_id.jackson2.FriendlyIdJackson2Module 2 | -------------------------------------------------------------------------------- /friendly-id-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | com.devskiller.friendly_id.boot.FriendlyIdAutoConfiguration 2 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | wrapperVersion=3.3.4 2 | distributionType=only-script 3 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip 4 | -------------------------------------------------------------------------------- /.sdkmanrc: -------------------------------------------------------------------------------- 1 | # SDKMAN configuration for friendly-id project 2 | # Auto-switch to this Java version when entering the directory 3 | # Usage: Enable auto-env in SDKMAN with: sdk config 4 | # Set sdkman_auto_env=true 5 | java=21.0.8-tem 6 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/domain/Foo.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.hateos.domain; 2 | 3 | import java.util.UUID; 4 | 5 | public record Foo(UUID id, String name) { 6 | } 7 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/domain/Bar.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.hateos.domain; 2 | 3 | import java.util.UUID; 4 | 5 | public record Bar(UUID id, String name, Foo foo) { 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | group: edge 4 | 5 | language: java 6 | 7 | jdk: 8 | - oraclejdk8 9 | - oraclejdk11 10 | 11 | script: ./mvnw package 12 | 13 | after_success: 14 | - ./mvnw clean test jacoco:report coveralls:report 15 | 16 | cache: 17 | directories: 18 | - ~/.m2/repository 19 | - ~/.m2/wrapper -------------------------------------------------------------------------------- /friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIdFormat.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | /** 4 | * Friendly ID format 5 | */ 6 | public enum FriendlyIdFormat { 7 | 8 | /** 9 | * Url62 encoded ID 10 | */ 11 | URL62, 12 | 13 | /** 14 | * Leave this ID as is (without conversion) 15 | */ 16 | RAW 17 | } 18 | -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/Bar.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import java.util.UUID; 4 | 5 | import com.devskiller.friendly_id.FriendlyIdFormat; 6 | import com.devskiller.friendly_id.IdFormat; 7 | 8 | public record Bar( 9 | @IdFormat(FriendlyIdFormat.RAW) UUID rawUuid, 10 | UUID friendlyId 11 | ) {} 12 | -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/Foo.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import java.util.UUID; 4 | 5 | import com.devskiller.friendly_id.FriendlyIdFormat; 6 | import com.devskiller.friendly_id.IdFormat; 7 | 8 | public record Foo( 9 | @IdFormat(FriendlyIdFormat.RAW) UUID rawUuid, 10 | UUID friendlyId 11 | ) {} 12 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductRequest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.jpa; 2 | 3 | import java.math.BigDecimal; 4 | 5 | /** 6 | * DTO for creating/updating products. 7 | */ 8 | public record ProductRequest( 9 | String name, 10 | String description, 11 | BigDecimal price, 12 | Integer stock 13 | ) { 14 | } 15 | -------------------------------------------------------------------------------- /friendly-id/src/jmh/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} ${LEVEL:-%6p} [%-9t] %-42logger{39} : %m%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/AdminUnauthorized.groovy: -------------------------------------------------------------------------------- 1 | package contracts 2 | 3 | import org.springframework.cloud.contract.spec.Contract 4 | 5 | Contract.make { 6 | description "should return 401 when accessing admin endpoint without authentication" 7 | 8 | request { 9 | method 'GET' 10 | url '/admin/status' 11 | } 12 | 13 | response { 14 | status 401 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /friendly-id/src/main/java/com/devskiller/friendly_id/IdFormat.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | /** 7 | * Declares that a field should be formatted as a friendly ID. 8 | */ 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface IdFormat { 11 | 12 | FriendlyIdFormat value() default FriendlyIdFormat.URL62; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /friendly-id/src/main/java/com/devskiller/friendly_id/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * FriendlyId - URL-friendly Base62 encoding of UUIDs. 3 | *

4 | * This package is null-marked, meaning all parameters and return values 5 | * are non-null by default unless explicitly annotated with {@link org.jspecify.annotations.Nullable}. 6 | */ 7 | @NullMarked 8 | package com.devskiller.friendly_id; 9 | 10 | import org.jspecify.annotations.NullMarked; 11 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/authenticated/AdminAuthorized.groovy: -------------------------------------------------------------------------------- 1 | package contracts.authenticated 2 | 3 | import org.springframework.cloud.contract.spec.Contract 4 | 5 | Contract.make { 6 | description "should return OK when accessing admin endpoint with authentication" 7 | 8 | request { 9 | method 'GET' 10 | url '/admin/status' 11 | } 12 | 13 | response { 14 | status 200 15 | body "OK" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /friendly-id/src/test/java/com/devskiller/friendly_id/IdUtil.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import java.util.Objects; 4 | 5 | class IdUtil { 6 | 7 | static boolean areEqualIgnoringLeadingZeros(String code1, String code2) { 8 | return Objects.equals(removeLeadingZeros(code1), removeLeadingZeros(code2)); 9 | } 10 | 11 | private static String removeLeadingZeros(String string) { 12 | return string.replaceFirst("^0+(?!$)", ""); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/Application.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.contracts; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/Application.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.hateos; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/Application.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.customized; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/CreateItemUnauthorized.groovy: -------------------------------------------------------------------------------- 1 | package contracts 2 | 3 | import org.springframework.cloud.contract.spec.Contract 4 | 5 | Contract.make { 6 | description "should return 401 when creating item without authentication" 7 | 8 | request { 9 | method 'POST' 10 | url '/items' 11 | headers { 12 | contentType applicationJson() 13 | } 14 | body( 15 | id: "unauthorizedItem" 16 | ) 17 | } 18 | 19 | response { 20 | status 401 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/AdminController.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.contracts; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @RestController 8 | @RequestMapping("/admin") 9 | public class AdminController { 10 | 11 | @GetMapping("/status") 12 | public String status() { 13 | return "OK"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /friendly-id-jpa/src/main/java/com/devskiller/friendly_id/jpa/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * JPA integration for FriendlyId. 3 | *

4 | * This package provides JPA AttributeConverter for automatic conversion between 5 | * FriendlyId value objects and UUID database columns. 6 | *

7 | *

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

11 | * 12 | * @see com.devskiller.friendly_id.jpa.FriendlyIdConverter 13 | * @since 1.1.1 14 | */ 15 | package com.devskiller.friendly_id.jpa; 16 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/GetItem.groovy: -------------------------------------------------------------------------------- 1 | package contracts 2 | 3 | import org.springframework.cloud.contract.spec.Contract 4 | 5 | Contract.make { 6 | description "should return item with all ID formats" 7 | 8 | request { 9 | method 'GET' 10 | url '/items/testItemId' 11 | } 12 | 13 | response { 14 | status 200 15 | headers { 16 | contentType applicationJson() 17 | } 18 | body( 19 | id: "testItemId", 20 | rawId: $(regex('[a-f0-9-]{36}')), 21 | friendlyUuid: "testItemId", 22 | friendlyId: "testItemId" 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/ObjectMapperConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import com.fasterxml.jackson.databind.Module; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | import com.devskiller.friendly_id.jackson2.FriendlyIdJackson2Module; 7 | 8 | public class ObjectMapperConfiguration { 9 | 10 | protected static ObjectMapper mapper(Module... modules) { 11 | ObjectMapper mapper = new ObjectMapper(); 12 | mapper.registerModule(new FriendlyIdJackson2Module()); 13 | mapper.registerModules(modules); 14 | return mapper; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /friendly-id/src/main/java/com/devskiller/friendly_id/UuidConverter.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import java.math.BigInteger; 4 | import java.util.UUID; 5 | 6 | class UuidConverter { 7 | 8 | static BigInteger toBigInteger(UUID uuid) { 9 | return BigIntegerPairing.pair( 10 | BigInteger.valueOf(uuid.getMostSignificantBits()), 11 | BigInteger.valueOf(uuid.getLeastSignificantBits()) 12 | ); 13 | } 14 | 15 | static UUID toUuid(BigInteger value) { 16 | BigInteger[] unpaired = BigIntegerPairing.unpair(value); 17 | return new UUID(unpaired[0].longValueExact(), unpaired[1].longValueExact()); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/ObjectMapperConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import tools.jackson.databind.JacksonModule; 4 | import tools.jackson.databind.json.JsonMapper; 5 | 6 | import com.devskiller.friendly_id.jackson.FriendlyIdModule; 7 | 8 | public class ObjectMapperConfiguration { 9 | 10 | protected static JsonMapper mapper(JacksonModule... modules) { 11 | JsonMapper.Builder builder = JsonMapper.builder() 12 | .addModule(new FriendlyIdModule()); 13 | for (JacksonModule module : modules) { 14 | builder.addModule(module); 15 | } 16 | return builder.build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * jOOQ integration for FriendlyId. 3 | *

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

7 | * 8 | *

Usage

9 | *

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

14 | * 15 | * @see com.devskiller.friendly_id.jooq.FriendlyIdConverter 16 | */ 17 | package com.devskiller.friendly_id.jooq; 18 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/BarResource.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.hateos; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.NoArgsConstructor; 7 | import org.springframework.hateoas.RepresentationModel; 8 | import org.springframework.hateoas.server.core.Relation; 9 | 10 | @Relation(value = "bar", collectionRelation = "bars") 11 | @Data 12 | @EqualsAndHashCode(callSuper = true) 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | class BarResource extends RepresentationModel { 16 | 17 | String name; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/Bar.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import java.util.UUID; 4 | 5 | import com.devskiller.friendly_id.FriendlyIdFormat; 6 | import com.devskiller.friendly_id.IdFormat; 7 | 8 | public class Bar { 9 | 10 | @IdFormat(FriendlyIdFormat.RAW) 11 | private final UUID rawUuid; 12 | 13 | private final UUID friendlyId; 14 | 15 | public Bar(UUID rawUuid, UUID friendlyId) { 16 | this.rawUuid = rawUuid; 17 | this.friendlyId = friendlyId; 18 | } 19 | 20 | public UUID getRawUuid() { 21 | return rawUuid; 22 | } 23 | 24 | public UUID getFriendlyId() { 25 | return friendlyId; 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /friendly-id/src/main/java/com/devskiller/friendly_id/type/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Value objects for FriendlyId domain model. 3 | *

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

7 | *

8 | * This package is null-marked, meaning all parameters and return values 9 | * are non-null by default unless explicitly annotated with {@link org.jspecify.annotations.Nullable}. 10 | *

11 | * 12 | * @see com.devskiller.friendly_id.type.FriendlyId 13 | * @since 1.1.1 14 | */ 15 | @NullMarked 16 | package com.devskiller.friendly_id.type; 17 | 18 | import org.jspecify.annotations.NullMarked; 19 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/test/resources/contracts/authenticated/CreateItem.groovy: -------------------------------------------------------------------------------- 1 | package contracts.authenticated 2 | 3 | import org.springframework.cloud.contract.spec.Contract 4 | 5 | Contract.make { 6 | description "should create item when authenticated" 7 | 8 | request { 9 | method 'POST' 10 | url '/items' 11 | headers { 12 | contentType applicationJson() 13 | } 14 | body( 15 | id: "authItemId" 16 | ) 17 | } 18 | 19 | response { 20 | status 200 21 | headers { 22 | contentType applicationJson() 23 | } 24 | body( 25 | id: "authItemId", 26 | rawId: $(regex('[a-f0-9-]{36}')), 27 | friendlyUuid: "authItemId", 28 | friendlyId: "authItemId" 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/Foo.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import java.util.UUID; 4 | 5 | import com.devskiller.friendly_id.FriendlyIdFormat; 6 | import com.devskiller.friendly_id.IdFormat; 7 | 8 | public class Foo { 9 | 10 | @IdFormat(FriendlyIdFormat.RAW) 11 | private UUID rawUuid; 12 | 13 | private UUID friendlyId; 14 | 15 | public UUID getRawUuid() { 16 | return rawUuid; 17 | } 18 | 19 | public void setRawUuid(UUID rawUuid) { 20 | this.rawUuid = rawUuid; 21 | } 22 | 23 | public UUID getFriendlyId() { 24 | return friendlyId; 25 | } 26 | 27 | public void setFriendlyId(UUID friendlyId) { 28 | this.friendlyId = friendlyId; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /friendly-id/src/main/java/com/devskiller/friendly_id/Url62.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import java.math.BigInteger; 4 | import java.util.UUID; 5 | 6 | /** 7 | * Class to convert UUID to Url62 IDs 8 | */ 9 | class Url62 { 10 | 11 | /** 12 | * Encode UUID to Url62 id 13 | * 14 | * @param uuid UUID to be encoded 15 | * @return url62 encoded UUID 16 | */ 17 | static String encode(UUID uuid) { 18 | BigInteger pair = UuidConverter.toBigInteger(uuid); 19 | return Base62.encode(pair); 20 | } 21 | 22 | /** 23 | * Decode url62 id to UUID 24 | * 25 | * @param id url62 encoded id 26 | * @return decoded UUID 27 | */ 28 | static UUID decode(String id) { 29 | BigInteger decoded = Base62.decode(id); 30 | return UuidConverter.toUuid(decoded); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: 4 | push: 5 | branches: [ master, feature/** ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up JDK 21 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: '21' 21 | distribution: 'temurin' 22 | cache: 'maven' 23 | 24 | - name: Build with Maven 25 | run: mvn -B clean install --file pom.xml 26 | 27 | - name: Upload coverage to Coveralls 28 | if: github.event_name != 'pull_request' 29 | run: mvn coveralls:report -DrepoToken=${{ secrets.COVERALLS_TOKEN }} 30 | continue-on-error: true 31 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdSerializer.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jackson2; 2 | 3 | import java.io.IOException; 4 | import java.util.UUID; 5 | 6 | import com.fasterxml.jackson.core.JsonGenerator; 7 | import com.fasterxml.jackson.databind.SerializerProvider; 8 | import com.fasterxml.jackson.databind.ser.std.StdSerializer; 9 | 10 | import com.devskiller.friendly_id.FriendlyIds; 11 | 12 | public class FriendlyIdSerializer extends StdSerializer { 13 | 14 | public FriendlyIdSerializer() { 15 | super(UUID.class); 16 | } 17 | 18 | @Override 19 | public void serialize(UUID uuid, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { 20 | jsonGenerator.writeString(FriendlyIds.toFriendlyId(uuid)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueSerializer.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jackson2; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.core.JsonGenerator; 6 | import com.fasterxml.jackson.databind.JsonSerializer; 7 | import com.fasterxml.jackson.databind.SerializerProvider; 8 | 9 | import com.devskiller.friendly_id.type.FriendlyId; 10 | 11 | /** 12 | * JSON serializer for {@link FriendlyId} value object. 13 | * Serializes FriendlyId instances as their string representation. 14 | */ 15 | public class FriendlyIdValueSerializer extends JsonSerializer { 16 | 17 | @Override 18 | public void serialize(FriendlyId value, JsonGenerator gen, SerializerProvider serializers) throws IOException { 19 | gen.writeString(value.toString()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueSerializer.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jackson; 2 | 3 | import tools.jackson.core.JsonGenerator; 4 | import tools.jackson.databind.SerializationContext; 5 | import tools.jackson.databind.ser.std.StdSerializer; 6 | 7 | import com.devskiller.friendly_id.type.FriendlyId; 8 | 9 | /** 10 | * JSON serializer for {@link FriendlyId} value object. 11 | * Serializes FriendlyId instances as their string representation. 12 | */ 13 | public class FriendlyIdValueSerializer extends StdSerializer { 14 | 15 | public FriendlyIdValueSerializer() { 16 | super(FriendlyId.class); 17 | } 18 | 19 | @Override 20 | public void serialize(FriendlyId value, JsonGenerator gen, SerializationContext ctxt) { 21 | gen.writeString(value.toString()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.jpa; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import com.devskiller.friendly_id.type.FriendlyId; 9 | 10 | /** 11 | * Spring Data JPA repository for Product entities. 12 | *

13 | * Note: The repository uses FriendlyId as the ID type, not UUID. 14 | * Spring Data automatically handles the FriendlyId type. 15 | *

16 | */ 17 | @Repository 18 | public interface ProductRepository extends JpaRepository { 19 | 20 | /** 21 | * Find product by name (case-insensitive). 22 | */ 23 | Optional findByNameIgnoreCase(String name); 24 | } 25 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdValueDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jackson2; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.core.JsonParser; 6 | import com.fasterxml.jackson.databind.DeserializationContext; 7 | import com.fasterxml.jackson.databind.JsonDeserializer; 8 | 9 | import com.devskiller.friendly_id.type.FriendlyId; 10 | 11 | /** 12 | * JSON deserializer for {@link FriendlyId} value object. 13 | * Deserializes JSON strings to FriendlyId instances. 14 | */ 15 | public class FriendlyIdValueDeserializer extends JsonDeserializer { 16 | 17 | @Override 18 | public FriendlyId deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { 19 | String friendlyIdString = p.getValueAsString(); 20 | return FriendlyId.parse(friendlyIdString); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/ItemService.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.customized; 2 | 3 | import java.util.UUID; 4 | 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import org.springframework.stereotype.Service; 8 | 9 | import com.devskiller.friendly_id.type.FriendlyId; 10 | 11 | @Slf4j 12 | @Service 13 | public class ItemService { 14 | 15 | public Item find(UUID uuid) { 16 | log.info("find: {}", uuid); 17 | return new Item(uuid, uuid, uuid, FriendlyId.of(uuid)); 18 | } 19 | 20 | public Item create(Item item) { 21 | if (item.id() == null) { 22 | var uuid = UUID.randomUUID(); 23 | return new Item(uuid, uuid, uuid, FriendlyId.of(uuid)); 24 | } 25 | return item; 26 | } 27 | 28 | public void update(UUID id, Item item) { 29 | log.info("update: {}:{}", id, item); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdValueDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jackson; 2 | 3 | import tools.jackson.core.JsonParser; 4 | import tools.jackson.databind.DeserializationContext; 5 | import tools.jackson.databind.deser.std.StdDeserializer; 6 | 7 | import com.devskiller.friendly_id.type.FriendlyId; 8 | 9 | /** 10 | * JSON deserializer for {@link FriendlyId} value object. 11 | * Deserializes JSON strings to FriendlyId instances. 12 | */ 13 | public class FriendlyIdValueDeserializer extends StdDeserializer { 14 | 15 | public FriendlyIdValueDeserializer() { 16 | super(FriendlyId.class); 17 | } 18 | 19 | @Override 20 | public FriendlyId deserialize(JsonParser p, DeserializationContext ctxt) { 21 | String friendlyIdString = p.getString(); 22 | return FriendlyId.parse(friendlyIdString); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/Item.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.contracts; 2 | 3 | import java.util.UUID; 4 | 5 | import com.devskiller.friendly_id.FriendlyIdFormat; 6 | import com.devskiller.friendly_id.IdFormat; 7 | import com.devskiller.friendly_id.type.FriendlyId; 8 | 9 | /** 10 | * Example record demonstrating different UUID serialization formats. 11 | * 12 | * @param id UUID serialized as FriendlyId string (default behavior) 13 | * @param rawId UUID serialized as raw UUID string 14 | * @param friendlyUuid UUID explicitly serialized as FriendlyId string 15 | * @param friendlyId FriendlyId value object type 16 | */ 17 | public record Item( 18 | UUID id, 19 | @IdFormat(FriendlyIdFormat.RAW) UUID rawId, 20 | @IdFormat(FriendlyIdFormat.URL62) UUID friendlyUuid, 21 | FriendlyId friendlyId 22 | ) { 23 | } 24 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Item.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.simple; 2 | 3 | import java.util.UUID; 4 | 5 | import com.devskiller.friendly_id.FriendlyIdFormat; 6 | import com.devskiller.friendly_id.IdFormat; 7 | import com.devskiller.friendly_id.type.FriendlyId; 8 | 9 | /** 10 | * Example record demonstrating different UUID serialization formats. 11 | * 12 | * @param id UUID serialized as FriendlyId string (default behavior) 13 | * @param rawId UUID serialized as raw UUID string 14 | * @param friendlyUuid UUID explicitly serialized as FriendlyId string 15 | * @param friendlyId FriendlyId value object type 16 | */ 17 | public record Item( 18 | UUID id, 19 | @IdFormat(FriendlyIdFormat.RAW) UUID rawId, 20 | @IdFormat(FriendlyIdFormat.URL62) UUID friendlyUuid, 21 | FriendlyId friendlyId 22 | ) { 23 | } 24 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Item.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.spring3; 2 | 3 | import java.util.UUID; 4 | 5 | import com.devskiller.friendly_id.FriendlyIdFormat; 6 | import com.devskiller.friendly_id.IdFormat; 7 | import com.devskiller.friendly_id.type.FriendlyId; 8 | 9 | /** 10 | * Example record demonstrating different UUID serialization formats. 11 | * 12 | * @param id UUID serialized as FriendlyId string (default behavior) 13 | * @param rawId UUID serialized as raw UUID string 14 | * @param friendlyUuid UUID explicitly serialized as FriendlyId string 15 | * @param friendlyId FriendlyId value object type 16 | */ 17 | public record Item( 18 | UUID id, 19 | @IdFormat(FriendlyIdFormat.RAW) UUID rawId, 20 | @IdFormat(FriendlyIdFormat.URL62) UUID friendlyUuid, 21 | FriendlyId friendlyId 22 | ) { 23 | } 24 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/Item.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.customized; 2 | 3 | import java.util.UUID; 4 | 5 | import com.devskiller.friendly_id.FriendlyIdFormat; 6 | import com.devskiller.friendly_id.IdFormat; 7 | import com.devskiller.friendly_id.type.FriendlyId; 8 | 9 | /** 10 | * Example record demonstrating different UUID serialization formats. 11 | * 12 | * @param id UUID serialized as FriendlyId string (default behavior) 13 | * @param rawId UUID serialized as raw UUID string 14 | * @param friendlyUuid UUID explicitly serialized as FriendlyId string 15 | * @param friendlyId FriendlyId value object type 16 | */ 17 | public record Item( 18 | UUID id, 19 | @IdFormat(FriendlyIdFormat.RAW) UUID rawId, 20 | @IdFormat(FriendlyIdFormat.URL62) UUID friendlyUuid, 21 | FriendlyId friendlyId 22 | ) { 23 | } 24 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | server.port=8090 3 | 4 | # H2 Database Configuration 5 | spring.datasource.url=jdbc:h2:mem:friendlyid_demo 6 | spring.datasource.driverClassName=org.h2.Driver 7 | spring.datasource.username=sa 8 | spring.datasource.password= 9 | 10 | # JPA/Hibernate Configuration 11 | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 12 | spring.jpa.hibernate.ddl-auto=create-drop 13 | spring.jpa.show-sql=true 14 | spring.jpa.properties.hibernate.format_sql=true 15 | 16 | # H2 Console (accessible at http://localhost:8080/h2-console) 17 | spring.h2.console.enabled=true 18 | spring.h2.console.path=/h2-console 19 | 20 | # Logging 21 | logging.level.org.hibernate.SQL=DEBUG 22 | logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE 23 | logging.level.com.devskiller.friendly_id=DEBUG 24 | logging.level.org.springframework.web=DEBUG 25 | logging.level.com.fasterxml.jackson=DEBUG 26 | -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/FriendlyIdDeserializerTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import java.util.UUID; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import com.devskiller.friendly_id.FriendlyIds; 8 | 9 | import static com.devskiller.friendly_id.spring.ObjectMapperConfiguration.mapper; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | class FriendlyIdDeserializerTest { 13 | 14 | @Test 15 | void shouldSerializeFriendlyId() { 16 | UUID uuid = UUID.randomUUID(); 17 | String json = mapper().writeValueAsString(uuid); 18 | System.out.println(json); 19 | assertThat(json).contains(FriendlyIds.toFriendlyId(uuid)); 20 | } 21 | 22 | @Test 23 | void shouldDeserializeFriendlyId() { 24 | String friendlyId = "2YSfgVHnEYbYgfFKhEX3Sz"; 25 | UUID uuid = mapper().readValue("\"" + friendlyId + "\"", UUID.class); 26 | assertThat(uuid).isEqualByComparingTo(FriendlyIds.toUuid(friendlyId)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jackson2; 2 | 3 | import java.io.IOException; 4 | import java.util.UUID; 5 | 6 | import com.fasterxml.jackson.core.JsonParser; 7 | import com.fasterxml.jackson.core.JsonToken; 8 | import com.fasterxml.jackson.databind.DeserializationContext; 9 | import com.fasterxml.jackson.databind.deser.std.UUIDDeserializer; 10 | 11 | import com.devskiller.friendly_id.FriendlyIds; 12 | 13 | public class FriendlyIdDeserializer extends UUIDDeserializer { 14 | 15 | @Override 16 | public UUID deserialize(JsonParser parser, DeserializationContext deserializationContext) throws IOException { 17 | JsonToken token = parser.getCurrentToken(); 18 | if (token == JsonToken.VALUE_STRING) { 19 | String value = parser.getValueAsString().trim(); 20 | return FriendlyIds.toUuid(value); 21 | } 22 | throw new IllegalStateException("Expected UUID or FriendlyId string"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/FriendlyIdDeserializerTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import java.util.UUID; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import com.devskiller.friendly_id.FriendlyIds; 8 | 9 | import static com.devskiller.friendly_id.spring.ObjectMapperConfiguration.mapper; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | class FriendlyIdDeserializerTest { 13 | 14 | @Test 15 | void shouldSerializeFriendlyId() throws Exception { 16 | UUID uuid = UUID.randomUUID(); 17 | String json = mapper().writeValueAsString(uuid); 18 | System.out.println(json); 19 | assertThat(json).contains(FriendlyIds.toFriendlyId(uuid)); 20 | } 21 | 22 | @Test 23 | void shouldDeserializeFriendlyId() throws Exception { 24 | String friendlyId = "2YSfgVHnEYbYgfFKhEX3Sz"; 25 | UUID uuid = mapper().readValue("\"" + friendlyId + "\"", UUID.class); 26 | assertThat(uuid).isEqualByComparingTo(FriendlyIds.toUuid(friendlyId)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/ContractVerifierBase.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.contracts; 2 | 3 | import io.restassured.module.mockmvc.RestAssuredMockMvc; 4 | import org.junit.jupiter.api.BeforeEach; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.web.context.WebApplicationContext; 10 | 11 | import com.devskiller.friendly_id.spring.EnableFriendlyId; 12 | 13 | import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; 14 | 15 | @WebMvcTest 16 | @EnableFriendlyId 17 | @Import(SecurityConfig.class) 18 | public abstract class ContractVerifierBase { 19 | 20 | @Autowired 21 | private WebApplicationContext context; 22 | 23 | @BeforeEach 24 | public void setUp() { 25 | RestAssuredMockMvc.webAppContextSetup(context, springSecurity()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /friendly-id-spring-boot-starter/src/main/java/com/devskiller/friendly_id/boot/FriendlyIdAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.boot; 2 | 3 | import org.springframework.boot.autoconfigure.AutoConfiguration; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 6 | 7 | import com.devskiller.friendly_id.spring.EnableFriendlyId; 8 | 9 | /** 10 | * Auto-configuration for FriendlyId integration with Spring Boot. 11 | *

12 | * Automatically enables FriendlyId converters and Jackson module when Spring Boot is detected. 13 | * Can be disabled by setting {@code com.devskiller.friendly-id.enabled=false} in application properties. 14 | */ 15 | @AutoConfiguration 16 | @ConditionalOnWebApplication 17 | @ConditionalOnProperty( 18 | prefix = "com.devskiller.friendly-id", 19 | name = "enabled", 20 | havingValue = "true", 21 | matchIfMissing = true 22 | ) 23 | @EnableFriendlyId 24 | public class FriendlyIdAutoConfiguration { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdJackson2Module.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jackson2; 2 | 3 | import java.util.UUID; 4 | 5 | import com.devskiller.friendly_id.type.FriendlyId; 6 | import com.fasterxml.jackson.databind.module.SimpleModule; 7 | 8 | public class FriendlyIdJackson2Module extends SimpleModule { 9 | 10 | private final FriendlyIdAnnotationIntrospector introspector; 11 | 12 | public FriendlyIdJackson2Module() { 13 | introspector = new FriendlyIdAnnotationIntrospector(); 14 | addDeserializer(UUID.class, new FriendlyIdDeserializer()); 15 | addSerializer(UUID.class, new FriendlyIdSerializer()); 16 | 17 | // Add serializer/deserializer for FriendlyId value object 18 | addDeserializer(FriendlyId.class, new FriendlyIdValueDeserializer()); 19 | addSerializer(FriendlyId.class, new FriendlyIdValueSerializer()); 20 | } 21 | 22 | @Override 23 | public void setupModule(SetupContext context) { 24 | super.setupModule(context); 25 | context.insertAnnotationIntrospector(introspector); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdModule.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jackson; 2 | 3 | import java.util.UUID; 4 | 5 | import tools.jackson.databind.module.SimpleModule; 6 | 7 | import com.devskiller.friendly_id.type.FriendlyId; 8 | 9 | /** 10 | * Jackson 3 module for FriendlyId serialization/deserialization. 11 | *

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

15 | */ 16 | public class FriendlyIdModule extends SimpleModule { 17 | 18 | public FriendlyIdModule() { 19 | super("FriendlyIdModule"); 20 | 21 | // UUID serializers/deserializers 22 | addSerializer(UUID.class, new FriendlyIdSerializer()); 23 | addDeserializer(UUID.class, new FriendlyIdDeserializer()); 24 | 25 | // FriendlyId value object serializers/deserializers 26 | addSerializer(FriendlyId.class, new FriendlyIdValueSerializer()); 27 | addDeserializer(FriendlyId.class, new FriendlyIdValueDeserializer()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/BarController.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.hateos; 2 | 3 | import com.devskiller.friendly_id.sample.hateos.domain.Bar; 4 | import com.devskiller.friendly_id.sample.hateos.domain.Foo; 5 | import org.springframework.hateoas.server.ExposesResourceFor; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.util.UUID; 12 | 13 | @RestController 14 | @ExposesResourceFor(BarResource.class) 15 | @RequestMapping("/foos/{fooId}/bars") 16 | public class BarController { 17 | 18 | private final BarResourceAssembler assembler; 19 | 20 | public BarController(BarResourceAssembler assembler) { 21 | this.assembler = assembler; 22 | } 23 | 24 | @GetMapping("/{id}") 25 | public BarResource getBar(@PathVariable UUID fooId, @PathVariable UUID id) { 26 | return assembler.toModel(new Bar(id, "Bar", new Foo(fooId, "Root Foo"))); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.contracts; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 7 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 8 | import org.springframework.security.web.SecurityFilterChain; 9 | 10 | @Configuration 11 | @EnableWebSecurity 12 | public class SecurityConfig { 13 | 14 | @Bean 15 | SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 16 | return http 17 | .authorizeHttpRequests(auth -> auth 18 | .requestMatchers("/admin/**").authenticated() 19 | .requestMatchers(org.springframework.http.HttpMethod.POST, "/items").authenticated() 20 | .anyRequest().permitAll() 21 | ) 22 | .csrf(AbstractHttpConfigurer::disable) 23 | .httpBasic(basic -> {}) 24 | .build(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/test/java/com/devskiller/friendly_id/sample/contracts/AuthenticatedContractBase.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.contracts; 2 | 3 | import io.restassured.module.mockmvc.RestAssuredMockMvc; 4 | import org.junit.jupiter.api.BeforeEach; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.security.test.context.support.WithMockUser; 10 | import org.springframework.web.context.WebApplicationContext; 11 | 12 | import com.devskiller.friendly_id.spring.EnableFriendlyId; 13 | 14 | import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; 15 | 16 | @WebMvcTest 17 | @EnableFriendlyId 18 | @WithMockUser(username = "admin", roles = "ADMIN") 19 | @Import(SecurityConfig.class) 20 | public abstract class AuthenticatedContractBase { 21 | 22 | @Autowired 23 | private WebApplicationContext context; 24 | 25 | @BeforeEach 26 | public void setUp() { 27 | RestAssuredMockMvc.webAppContextSetup(context, springSecurity()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-contracts/src/main/java/com/devskiller/friendly_id/sample/contracts/ItemController.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.contracts; 2 | 3 | import java.util.UUID; 4 | 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import com.devskiller.friendly_id.type.FriendlyId; 15 | 16 | @Slf4j 17 | @RestController 18 | @RequestMapping("/items") 19 | public class ItemController { 20 | 21 | @GetMapping("/{id}") 22 | public Item get(@PathVariable UUID id) { 23 | log.info("Get {}", id); 24 | return new Item(id, id, id, FriendlyId.of(id)); 25 | } 26 | 27 | @PostMapping 28 | public Item create(@RequestBody Item item) { 29 | log.info("Create {}", item); 30 | var uuid = item.id() != null ? item.id() : UUID.randomUUID(); 31 | return new Item(uuid, uuid, uuid, FriendlyId.of(uuid)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /friendly-id/src/test/java/com/devskiller/friendly_id/Url62Test.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 6 | 7 | class Url62Test { 8 | 9 | @Test 10 | void shouldExplodeWhenContainsIllegalCharacters() { 11 | assertThatThrownBy(() -> Url62.decode("Foo Bar")) 12 | .isInstanceOf(IllegalArgumentException.class) 13 | .hasMessageContaining("contains illegal characters"); 14 | } 15 | 16 | @Test 17 | void shouldFaildOnEmptyString() { 18 | assertThatThrownBy(() -> Url62.decode("")) 19 | .isInstanceOf(IllegalArgumentException.class) 20 | .hasMessageContaining("must not be empty"); 21 | } 22 | 23 | @Test 24 | void shouldFailsOnNullString() { 25 | assertThatThrownBy(() -> Url62.decode(null)) 26 | .isInstanceOf(NullPointerException.class) 27 | .hasMessageContaining("must not be null"); 28 | } 29 | 30 | @Test 31 | void shouldFailsWhenStringContainsMoreThan128bitInformation() { 32 | assertThatThrownBy(() -> Url62.decode("7NLCAyd6sKR7kDHxgAWFPas")) 33 | .isInstanceOf(IllegalArgumentException.class) 34 | .hasMessageContaining("contains more than 128bit information"); 35 | } 36 | } -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/BarResourceAssembler.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.hateos; 2 | 3 | import com.devskiller.friendly_id.sample.hateos.domain.Bar; 4 | import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; 5 | import org.springframework.stereotype.Component; 6 | 7 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; 8 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; 9 | 10 | @Component 11 | public class BarResourceAssembler extends RepresentationModelAssemblerSupport { 12 | 13 | public BarResourceAssembler() { 14 | super(BarController.class, BarResource.class); 15 | } 16 | 17 | @Override 18 | public BarResource toModel(Bar entity) { 19 | BarResource resource = new BarResource(entity.name()); 20 | 21 | // Modern Spring HATEOAS 2.x - methodOn() triggers automatic FriendlyId conversion 22 | resource.add(linkTo(FooController.class).withRel("foos")); 23 | resource.add(linkTo(methodOn(BarController.class) 24 | .getBar(entity.foo().id(), entity.id())) 25 | .withSelfRel()); 26 | 27 | return resource; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/Application.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.spring3; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import com.devskiller.friendly_id.type.FriendlyId; 14 | 15 | @RestController 16 | @SpringBootApplication 17 | public class Application { 18 | 19 | public static void main(String[] args) { 20 | SpringApplication.run(Application.class, args); 21 | } 22 | 23 | @GetMapping("/items/{id}") 24 | Item getItem(@PathVariable UUID id) { 25 | return new Item(id, id, id, FriendlyId.of(id)); 26 | } 27 | 28 | @PostMapping("/items") 29 | Item createItem(@RequestBody Item item) { 30 | if (item.id() == null) { 31 | var uuid = UUID.randomUUID(); 32 | return new Item(uuid, uuid, uuid, FriendlyId.of(uuid)); 33 | } 34 | return item; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /friendly-id/src/main/java/com/devskiller/friendly_id/BigIntegerPairing.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import java.math.BigInteger; 4 | import java.util.function.Function; 5 | 6 | /** 7 | * Basing on snippet published by drmalex07 8 | *

9 | * https://gist.github.com/drmalex07/9008c611ffde6cb2ef3a2db8668bc251 10 | */ 11 | class BigIntegerPairing { 12 | 13 | private static final BigInteger HALF = BigInteger.ONE.shiftLeft(64); // 2^64 14 | private static final BigInteger MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE); 15 | 16 | private static Function toUnsigned = value 17 | -> value.signum() < 0 ? value.add(HALF) : value; 18 | private static Function toSigned = 19 | value -> MAX_LONG.compareTo(value) < 0 ? value.subtract(HALF) : value; 20 | 21 | static BigInteger pair(BigInteger hi, BigInteger lo) { 22 | BigInteger unsignedLo = toUnsigned.apply(lo); 23 | BigInteger unsignedHi = toUnsigned.apply(hi); 24 | return unsignedLo.add(unsignedHi.multiply(HALF)); 25 | } 26 | 27 | static BigInteger[] unpair(BigInteger value) { 28 | BigInteger[] parts = value.divideAndRemainder(HALF); 29 | BigInteger signedHi = toSigned.apply(parts[0]); 30 | BigInteger signedLo = toSigned.apply(parts[1]); 31 | return new BigInteger[]{signedHi, signedLo}; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /friendly-id/src/test/java/com/devskiller/friendly_id/FriendlyIdsTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import java.math.BigInteger; 4 | import java.util.Random; 5 | import java.util.UUID; 6 | 7 | import org.junit.jupiter.api.RepeatedTest; 8 | 9 | import static com.devskiller.friendly_id.FriendlyIds.*; 10 | import static com.devskiller.friendly_id.IdUtil.areEqualIgnoringLeadingZeros; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | class FriendlyIdsTest { 14 | 15 | @RepeatedTest(1000) 16 | void shouldCreateValidIdsThatConformToUuidType4() { 17 | UUID uuid = toUuid(createFriendlyId()); 18 | assertThat(uuid.version()).isEqualTo(4); 19 | } 20 | 21 | @RepeatedTest(1000) 22 | void encodingUuidShouldBeReversible() { 23 | UUID uuid = UUID.randomUUID(); 24 | UUID result = toUuid(toFriendlyId(uuid)); 25 | assertThat(result).isEqualTo(uuid); 26 | } 27 | 28 | @RepeatedTest(1000) 29 | void decodingIdShouldBeReversible() { 30 | String id = generateRandomFriendlyId(); 31 | String result = toFriendlyId(toUuid(id)); 32 | assertThat(areEqualIgnoringLeadingZeros(result, id)).isTrue(); 33 | } 34 | 35 | private String generateRandomFriendlyId() { 36 | Random random = new Random(); 37 | BigInteger bigInt = new BigInteger(128, random); 38 | return Base62.encode(bigInt); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /friendly-id/src/test/java/com/devskiller/friendly_id/AnalyzeGeneratedIdsTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.IntSummaryStatistics; 7 | import java.util.List; 8 | import java.util.UUID; 9 | import java.util.stream.Collectors; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | class AnalyzeGeneratedIdsTest { 14 | 15 | private List ids = new ArrayList<>(); 16 | 17 | @Test 18 | void analyzeGeneratedValueStatistics() { 19 | for (int i = 0; i < 100_000; i++) { 20 | this.ids.add(Base62.encode(UuidConverter.toBigInteger(UUID.randomUUID()))); 21 | } 22 | IntSummaryStatistics stats = ids.stream().map(String::length).mapToInt(Integer::intValue).summaryStatistics(); 23 | 24 | System.out.println("\nResults:"); 25 | System.out.println("Min: " + stats.getMin()); 26 | System.out.println("Max: " + stats.getMax()); 27 | System.out.println("Avg: " + stats.getAverage()); 28 | System.out.println("Count: " + stats.getCount()); 29 | System.out.println("Sample: \n" + ids.stream().limit(100).collect(Collectors.joining("\n"))); 30 | 31 | assertThat(stats.getMax()).isEqualTo(22); 32 | assertThat(stats.getMin()).isGreaterThanOrEqualTo(17); 33 | assertThat(stats.getAverage()).isLessThanOrEqualTo(22); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-customized/src/main/java/com/devskiller/friendly_id/sample/customized/ItemController.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.customized; 2 | 3 | import java.util.UUID; 4 | 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.PutMapping; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | @Slf4j 16 | @RestController 17 | @RequestMapping("/items") 18 | public class ItemController { 19 | 20 | private final ItemService itemService; 21 | 22 | public ItemController(ItemService itemService) { 23 | this.itemService = itemService; 24 | } 25 | 26 | @GetMapping("/{id}") 27 | public Item get(@PathVariable UUID id) { 28 | log.info("get {}", id); 29 | return itemService.find(id); 30 | } 31 | 32 | @PostMapping 33 | public Item create(@RequestBody Item item) { 34 | log.info("create {}", item); 35 | return itemService.create(item); 36 | } 37 | 38 | @PutMapping("/{id}") 39 | public void update(@PathVariable UUID id, @RequestBody Item body) { 40 | itemService.update(id, body); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /friendly-id-spring-boot/src/main/java/com/devskiller/friendly_id/spring/EnableFriendlyId.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | import org.springframework.context.annotation.Import; 10 | 11 | /** 12 | * Enable FriendlyId support in Spring MVC applications. 13 | *

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

    17 | *
  • Path variables ({@code @PathVariable UUID id})
  • 18 | *
  • Request parameters ({@code @RequestParam UUID id})
  • 19 | *
  • JSON request/response bodies (via Jackson)
  • 20 | *
21 | *

22 | * Example usage: 23 | *

24 |  * @Configuration
25 |  * @EnableFriendlyId
26 |  * public class WebConfig {
27 |  * }
28 |  * 
29 | *

30 | * Note: When using {@code spring-boot-starter-friendly-id}, this configuration 31 | * is applied automatically and this annotation is not required. 32 | * 33 | * @see FriendlyIdConfiguration 34 | */ 35 | @Target(ElementType.TYPE) 36 | @Retention(RetentionPolicy.RUNTIME) 37 | @Documented 38 | @Import(FriendlyIdConfiguration.class) 39 | public @interface EnableFriendlyId { 40 | 41 | } 42 | -------------------------------------------------------------------------------- /friendly-id/src/test/java/com/devskiller/friendly_id/BigIntegerPairingTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import org.junit.jupiter.api.RepeatedTest; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.math.BigInteger; 7 | import java.util.Random; 8 | 9 | import static com.devskiller.friendly_id.BigIntegerPairing.pair; 10 | import static com.devskiller.friendly_id.BigIntegerPairing.unpair; 11 | import static java.math.BigInteger.valueOf; 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | class BigIntegerPairingTest { 15 | 16 | @Test 17 | void shouldPairTwoLongs() { 18 | long x = 1; 19 | long y = 2; 20 | 21 | BigInteger z = pair(valueOf(1), valueOf(2)); 22 | 23 | assertThat(unpair(z)).contains(valueOf(x), valueOf(y)); 24 | } 25 | 26 | @RepeatedTest(1000) 27 | void resultOfPairingShouldBePositive() { 28 | Random random = new Random(); 29 | long x = random.nextLong(); 30 | long y = random.nextLong(); 31 | 32 | BigInteger paired = pair(valueOf(x), valueOf(y)); 33 | 34 | assertThat(paired.signum()).isGreaterThan(0); 35 | } 36 | 37 | @RepeatedTest(1000) 38 | void pairingLongsShouldBeReversible() { 39 | Random random = new Random(); 40 | long x = random.nextLong(); 41 | long y = random.nextLong(); 42 | 43 | BigInteger paired = pair(valueOf(x), valueOf(y)); 44 | BigInteger[] unpaired = unpair(paired); 45 | 46 | assertThat(unpaired).containsExactly(valueOf(x), valueOf(y)); 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyId.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | * Class to convert UUID to url Friendly IDs basing on Url62. 7 | * 8 | * @deprecated Use {@link FriendlyIds} instead. This class will be removed in a future version. 9 | */ 10 | @Deprecated(since = "2.0", forRemoval = true) 11 | public class FriendlyId { 12 | 13 | /** 14 | * Create FriendlyId id 15 | * 16 | * @return Friendly Id encoded UUID 17 | * @deprecated Use {@link FriendlyIds#createFriendlyId()} instead. 18 | */ 19 | @Deprecated(since = "2.0", forRemoval = true) 20 | public static String createFriendlyId() { 21 | return FriendlyIds.createFriendlyId(); 22 | } 23 | 24 | /** 25 | * Encode UUID to FriendlyId id 26 | * 27 | * @param uuid UUID to be encoded 28 | * @return Friendly Id encoded UUID 29 | * @deprecated Use {@link FriendlyIds#toFriendlyId(UUID)} instead. 30 | */ 31 | @Deprecated(since = "2.0", forRemoval = true) 32 | public static String toFriendlyId(UUID uuid) { 33 | return FriendlyIds.toFriendlyId(uuid); 34 | } 35 | 36 | /** 37 | * Decode Friendly Id to UUID 38 | * 39 | * @param friendlyId encoded UUID 40 | * @return decoded UUID 41 | * @deprecated Use {@link FriendlyIds#toUuid(String)} instead. 42 | */ 43 | @Deprecated(since = "2.0", forRemoval = true) 44 | public static UUID toUuid(String friendlyId) { 45 | return FriendlyIds.toUuid(friendlyId); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot3-simple/src/main/java/com/devskiller/friendly_id/sample/spring3/FriendlyIdConfig.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.spring3; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.convert.converter.Converter; 8 | import org.springframework.format.FormatterRegistry; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | 11 | import com.devskiller.friendly_id.FriendlyIds; 12 | import com.devskiller.friendly_id.jackson2.FriendlyIdJackson2Module; 13 | 14 | @Configuration 15 | public class FriendlyIdConfig implements WebMvcConfigurer { 16 | 17 | @Override 18 | public void addFormatters(FormatterRegistry registry) { 19 | registry.addConverter(new StringToUuidConverter()); 20 | registry.addConverter(new UuidToStringConverter()); 21 | } 22 | 23 | @Bean 24 | FriendlyIdJackson2Module friendlyIdModule() { 25 | return new FriendlyIdJackson2Module(); 26 | } 27 | 28 | public static class StringToUuidConverter implements Converter { 29 | 30 | @Override 31 | public UUID convert(String id) { 32 | return FriendlyIds.toUuid(id); 33 | } 34 | } 35 | 36 | public static class UuidToStringConverter implements Converter { 37 | 38 | @Override 39 | public String convert(UUID id) { 40 | return FriendlyIds.toFriendlyId(id); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /friendly-id-jooq/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | friendly-id-project 7 | com.devskiller.friendly-id 8 | ${revision} 9 | .. 10 | 11 | 12 | friendly-id-jooq 13 | 14 | FriendlyId jOOQ Integration 15 | jOOQ converters for FriendlyId - enables transparent UUID to FriendlyId conversion in database queries 16 | 17 | 18 | 19 | com.devskiller.friendly-id 20 | friendly-id 21 | ${project.version} 22 | 23 | 24 | org.jooq 25 | jooq 26 | provided 27 | 28 | 29 | 30 | 31 | org.junit.jupiter 32 | junit-jupiter 33 | test 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up JDK 21 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '21' 20 | distribution: 'temurin' 21 | cache: 'maven' 22 | server-id: central 23 | server-username: MAVEN_USERNAME 24 | server-password: MAVEN_PASSWORD 25 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} 26 | gpg-passphrase: GPG_PASSPHRASE 27 | 28 | - name: Extract version from tag 29 | id: get_version 30 | run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 31 | 32 | - name: Build and deploy to Maven Central 33 | run: | 34 | mvn -B clean deploy -P release \ 35 | -Drevision=${{ steps.get_version.outputs.VERSION }} \ 36 | -DskipTests 37 | env: 38 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} 39 | MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} 40 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 41 | 42 | - name: Create GitHub Release 43 | uses: softprops/action-gh-release@v1 44 | with: 45 | generate_release_notes: true 46 | files: | 47 | **/target/*.jar 48 | !**/target/*-sources.jar 49 | !**/target/*-javadoc.jar 50 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooResource.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.hateos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 6 | import org.springframework.hateoas.CollectionModel; 7 | import org.springframework.hateoas.RepresentationModel; 8 | import org.springframework.hateoas.server.core.Relation; 9 | 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | @Relation(value = "foos") 14 | public class FooResource extends RepresentationModel { 15 | 16 | private final UUID uuid; 17 | private final String name; 18 | @JsonUnwrapped 19 | private final CollectionModel embeddeds; 20 | 21 | // Full constructor 22 | public FooResource(UUID uuid, String name, CollectionModel embeddeds) { 23 | this.uuid = uuid; 24 | this.name = name; 25 | this.embeddeds = embeddeds; 26 | } 27 | 28 | // Constructor for creating resources without embedded collections (for deserialization) 29 | @JsonCreator 30 | public FooResource(@JsonProperty("uuid") UUID uuid, @JsonProperty("name") String name) { 31 | this(uuid, name, CollectionModel.of(List.of())); 32 | } 33 | 34 | public UUID getUuid() { 35 | return uuid; 36 | } 37 | 38 | public String getName() { 39 | return name; 40 | } 41 | 42 | public CollectionModel getEmbeddeds() { 43 | return embeddeds; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /friendly-id-jpa/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | friendly-id-project 7 | com.devskiller.friendly-id 8 | ${revision} 9 | .. 10 | 11 | 12 | friendly-id-jpa 13 | 14 | FriendlyId JPA Integration 15 | JPA AttributeConverter for FriendlyId - enables transparent UUID to FriendlyId conversion in JPA entities 16 | 17 | 18 | 19 | com.devskiller.friendly-id 20 | friendly-id 21 | ${project.version} 22 | 23 | 24 | jakarta.persistence 25 | jakarta.persistence-api 26 | 3.1.0 27 | provided 28 | 29 | 30 | 31 | 32 | org.junit.jupiter 33 | junit-jupiter 34 | test 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /friendly-id-spring-boot/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.devskiller.friendly-id 7 | friendly-id-project 8 | ${revision} 9 | .. 10 | 11 | 12 | friendly-id-spring-boot 13 | 14 | FriendlyId Spring Boot 15 | Spring Boot integration for FriendlyId - provides converters and Jackson module configuration 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-web 21 | provided 22 | 23 | 24 | com.devskiller.friendly-id 25 | friendly-id-jackson-datatype 26 | ${project.version} 27 | 28 | 29 | 30 | tools.jackson.core 31 | jackson-databind 32 | provided 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdSerializer.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jackson; 2 | 3 | import java.util.UUID; 4 | 5 | import tools.jackson.core.JsonGenerator; 6 | import tools.jackson.databind.BeanProperty; 7 | import tools.jackson.databind.SerializationContext; 8 | import tools.jackson.databind.ValueSerializer; 9 | import tools.jackson.databind.ser.std.StdSerializer; 10 | 11 | import com.devskiller.friendly_id.FriendlyIds; 12 | import com.devskiller.friendly_id.FriendlyIdFormat; 13 | import com.devskiller.friendly_id.IdFormat; 14 | 15 | public class FriendlyIdSerializer extends StdSerializer { 16 | 17 | private final boolean useFriendlyFormat; 18 | 19 | public FriendlyIdSerializer() { 20 | this(true); 21 | } 22 | 23 | private FriendlyIdSerializer(boolean useFriendlyFormat) { 24 | super(UUID.class); 25 | this.useFriendlyFormat = useFriendlyFormat; 26 | } 27 | 28 | @Override 29 | public void serialize(UUID uuid, JsonGenerator jsonGenerator, SerializationContext ctxt) { 30 | if (useFriendlyFormat) { 31 | jsonGenerator.writeString(FriendlyIds.toFriendlyId(uuid)); 32 | } else { 33 | jsonGenerator.writeString(uuid.toString()); 34 | } 35 | } 36 | 37 | @Override 38 | public ValueSerializer createContextual(SerializationContext ctxt, BeanProperty property) { 39 | if (property != null) { 40 | IdFormat annotation = property.getAnnotation(IdFormat.class); 41 | if (annotation != null && annotation.value() == FriendlyIdFormat.RAW) { 42 | return new FriendlyIdSerializer(false); 43 | } 44 | } 45 | return this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdEncoder.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.openfeign; 2 | 3 | import java.lang.reflect.Type; 4 | import java.util.UUID; 5 | 6 | import com.devskiller.friendly_id.FriendlyIds; 7 | 8 | import feign.RequestTemplate; 9 | import feign.codec.EncodeException; 10 | import feign.codec.Encoder; 11 | 12 | /** 13 | * Feign encoder that converts UUID and FriendlyId objects to FriendlyId strings in requests. 14 | *

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

18 | *

19 | * Supported conversions: 20 | *

21 | *
    22 | *
  • {@link UUID} → FriendlyId string
  • 23 | *
  • {@link com.devskiller.friendly_id.type.FriendlyId} → FriendlyId string
  • 24 | *
25 | * 26 | * @see FriendlyIdDecoder 27 | * @since 1.1.1 28 | */ 29 | public class FriendlyIdEncoder implements Encoder { 30 | 31 | private final Encoder delegate; 32 | 33 | public FriendlyIdEncoder(Encoder delegate) { 34 | this.delegate = delegate; 35 | } 36 | 37 | @Override 38 | public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { 39 | if (object instanceof UUID uuid) { 40 | delegate.encode(FriendlyIds.toFriendlyId(uuid), bodyType, template); 41 | } else if (object instanceof com.devskiller.friendly_id.type.FriendlyId friendlyId) { 42 | delegate.encode(friendlyId.toString(), bodyType, template); 43 | } else { 44 | delegate.encode(object, bodyType, template); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-simple/src/main/java/com/devskiller/friendly_id/sample/simple/Application.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.simple; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import com.devskiller.friendly_id.type.FriendlyId; 15 | 16 | @RestController 17 | @SpringBootApplication 18 | public class Application { 19 | 20 | public static void main(String[] args) { 21 | SpringApplication.run(Application.class, args); 22 | } 23 | 24 | @GetMapping("/items/{id}") 25 | Item getItem(@PathVariable UUID id) { 26 | return new Item(id, id, id, FriendlyId.of(id)); 27 | } 28 | 29 | @PostMapping("/items") 30 | Item createItem(@RequestBody Item item) { 31 | if (item.id() == null) { 32 | var uuid = UUID.randomUUID(); 33 | return new Item(uuid, uuid, uuid, FriendlyId.of(uuid)); 34 | } 35 | return item; 36 | } 37 | 38 | @GetMapping("/items") 39 | Item getItemByParam(@RequestParam UUID id) { 40 | return new Item(id, id, id, FriendlyId.of(id)); 41 | } 42 | 43 | @GetMapping("/items/by-friendly-id") 44 | Item getItemByFriendlyIdParam(@RequestParam FriendlyId id) { 45 | var uuid = id.uuid(); 46 | return new Item(uuid, uuid, uuid, id); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdDecoder.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.openfeign; 2 | 3 | import java.io.IOException; 4 | import java.lang.reflect.Type; 5 | import java.util.UUID; 6 | 7 | import feign.FeignException; 8 | import feign.Response; 9 | import feign.codec.DecodeException; 10 | import feign.codec.Decoder; 11 | 12 | import com.devskiller.friendly_id.FriendlyIds; 13 | import com.devskiller.friendly_id.type.FriendlyId; 14 | 15 | /** 16 | * Feign decoder that converts FriendlyId strings to UUID and FriendlyId objects in responses. 17 | *

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

21 | *

22 | * Supported conversions: 23 | *

24 | *
    25 | *
  • FriendlyId string → {@link UUID}
  • 26 | *
  • FriendlyId string → {@link FriendlyId}
  • 27 | *
28 | * 29 | * @see FriendlyIdEncoder 30 | * @since 1.1.1 31 | */ 32 | public class FriendlyIdDecoder implements Decoder { 33 | 34 | private final Decoder delegate; 35 | 36 | public FriendlyIdDecoder(Decoder delegate) { 37 | this.delegate = delegate; 38 | } 39 | 40 | @Override 41 | public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException { 42 | Object decoded = delegate.decode(response, type); 43 | 44 | if (type == UUID.class && decoded instanceof String stringValue) { 45 | return FriendlyIds.toUuid(stringValue); 46 | } 47 | 48 | if (type == FriendlyId.class && decoded instanceof String stringValue) { 49 | return FriendlyId.parse(stringValue); 50 | } 51 | 52 | return decoded; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /friendly-id/src/test/java/com/devskiller/friendly_id/Base62Test.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import org.junit.jupiter.api.RepeatedTest; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.math.BigInteger; 7 | import java.util.Random; 8 | 9 | import static com.devskiller.friendly_id.IdUtil.areEqualIgnoringLeadingZeros; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 12 | 13 | class Base62Test { 14 | 15 | @Test 16 | void decodingValuePrefixedWithZeros() { 17 | assertThat(Base62.encode(Base62.decode("00001"))).isEqualTo("1"); 18 | assertThat(Base62.encode(Base62.decode("01001"))).isEqualTo("1001"); 19 | assertThat(Base62.encode(Base62.decode("00abcd"))).isEqualTo("abcd"); 20 | } 21 | 22 | @Test 23 | void shouldCheck128BitLimits() { 24 | assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> 25 | Base62.decode("1Vkp6axDWu5pI3q1xQO3oO0")); 26 | } 27 | 28 | @RepeatedTest(1000) 29 | void decodingIdShouldBeReversible() { 30 | String id = generateRandomFriendlyId(); 31 | String result = Base62.encode(Base62.decode(id)); 32 | assertThat(areEqualIgnoringLeadingZeros(result, id)).isTrue(); 33 | } 34 | 35 | @RepeatedTest(1000) 36 | void encodingNumberShouldBeReversible() { 37 | BigInteger bigInteger = new BigInteger(128, new Random()); 38 | BigInteger result = Base62.decode(Base62.encode(bigInteger)); 39 | assertThat(result).isEqualTo(bigInteger); 40 | } 41 | 42 | private String generateRandomFriendlyId() { 43 | Random random = new Random(); 44 | BigInteger bigInt = new BigInteger(128, random); 45 | return Base62.encode(bigInt); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/src/test/java/com/devskiller/friendly_id/sample/hateos/BarControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.hateos; 2 | 3 | import com.devskiller.friendly_id.spring.EnableFriendlyId; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; 7 | import org.springframework.context.annotation.Import; 8 | import org.springframework.test.web.servlet.MockMvc; 9 | 10 | import static org.hamcrest.CoreMatchers.is; 11 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 12 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 13 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 16 | 17 | @WebMvcTest(BarController.class) 18 | @EnableFriendlyId 19 | @Import({FooResourceAssembler.class, BarResourceAssembler.class}) 20 | class BarControllerTest { 21 | 22 | @Autowired 23 | MockMvc mockMvc; 24 | 25 | @Test 26 | void shouldGet() throws Exception { 27 | mockMvc.perform(get("/foos/{fooId}/bars/{barId}", "foo", "bar")) 28 | .andDo(print()) 29 | .andExpect(status().isOk()) 30 | .andExpect(content().contentType("application/hal+json")) 31 | .andExpect(jsonPath("$.name", is("Bar"))) 32 | .andExpect(jsonPath("$._links.self.href", is("http://localhost/foos/foo/bars/bar"))) 33 | .andExpect(jsonPath("$._links.foos.href", is("http://localhost/foos"))); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /friendly-id-spring-boot-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.devskiller.friendly-id 7 | friendly-id-project 8 | ${revision} 9 | .. 10 | 11 | 12 | friendly-id-spring-boot-starter 13 | 14 | FriendlyId Spring Boot Starter 15 | Spring Boot starter for FriendlyId - auto-configuration for easy integration 16 | 17 | 18 | 19 | com.devskiller.friendly-id 20 | friendly-id-spring-boot 21 | ${project.version} 22 | 23 | 24 | com.devskiller.friendly-id 25 | friendly-id-jackson-datatype 26 | ${project.version} 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter 31 | provided 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-autoconfigure-processor 36 | true 37 | 38 | 39 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooResourceAssembler.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.hateos; 2 | 3 | import com.devskiller.friendly_id.sample.hateos.domain.Bar; 4 | import com.devskiller.friendly_id.sample.hateos.domain.Foo; 5 | import org.springframework.hateoas.CollectionModel; 6 | import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; 14 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; 15 | 16 | @Component 17 | public class FooResourceAssembler extends RepresentationModelAssemblerSupport { 18 | 19 | private final BarResourceAssembler barResourceAssembler; 20 | 21 | public FooResourceAssembler(BarResourceAssembler barResourceAssembler) { 22 | super(FooController.class, FooResource.class); 23 | this.barResourceAssembler = barResourceAssembler; 24 | } 25 | 26 | @Override 27 | public FooResource toModel(Foo entity) { 28 | List bars = Arrays.asList( 29 | new Bar(UUID.randomUUID(), "bar one", entity), 30 | new Bar(UUID.randomUUID(), "bar two", entity) 31 | ); 32 | CollectionModel barResources = barResourceAssembler.toCollectionModel(bars); 33 | FooResource resource = new FooResource(entity.id(), entity.name(), barResources); 34 | 35 | // Modern Spring HATEOAS 2.x - methodOn() triggers automatic FriendlyId conversion 36 | resource.add(linkTo(methodOn(FooController.class) 37 | .get(entity.id())) 38 | .withSelfRel()); 39 | 40 | return resource; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /friendly-id-spring-boot/src/main/java/com/devskiller/friendly_id/spring/FriendlyIdConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import java.util.UUID; 4 | 5 | import tools.jackson.databind.JacksonModule; 6 | 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.format.FormatterRegistry; 10 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 11 | 12 | import com.devskiller.friendly_id.FriendlyIds; 13 | import com.devskiller.friendly_id.jackson.FriendlyIdModule; 14 | import com.devskiller.friendly_id.type.FriendlyId; 15 | 16 | /** 17 | * Configuration for FriendlyId integration with Spring MVC. 18 | *

19 | * This configuration: 20 | *

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

25 | * Enable this configuration by adding {@link EnableFriendlyId @EnableFriendlyId} to your configuration class, 26 | * or use the spring-boot-starter for automatic configuration. 27 | */ 28 | @Configuration 29 | public class FriendlyIdConfiguration implements WebMvcConfigurer { 30 | 31 | @Override 32 | public void addFormatters(FormatterRegistry registry) { 33 | registry.addConverter(String.class, UUID.class, FriendlyIds::toUuid); 34 | registry.addConverter(UUID.class, String.class, FriendlyIds::toFriendlyId); 35 | registry.addConverter(String.class, FriendlyId.class, FriendlyId::parse); 36 | registry.addConverter(FriendlyId.class, String.class, FriendlyId::toString); 37 | } 38 | 39 | @Bean 40 | public JacksonModule friendlyIdModule() { 41 | return new FriendlyIdModule(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/src/main/java/com/devskiller/friendly_id/jackson/FriendlyIdDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jackson; 2 | 3 | import java.util.UUID; 4 | 5 | import tools.jackson.core.JsonParser; 6 | import tools.jackson.core.JsonToken; 7 | import tools.jackson.databind.BeanProperty; 8 | import tools.jackson.databind.DeserializationContext; 9 | import tools.jackson.databind.ValueDeserializer; 10 | import tools.jackson.databind.deser.std.StdDeserializer; 11 | 12 | import com.devskiller.friendly_id.FriendlyIdFormat; 13 | import com.devskiller.friendly_id.IdFormat; 14 | 15 | import static com.devskiller.friendly_id.type.FriendlyId.parse; 16 | 17 | public class FriendlyIdDeserializer extends StdDeserializer { 18 | 19 | private final boolean useFriendlyFormat; 20 | 21 | public FriendlyIdDeserializer() { 22 | this(true); 23 | } 24 | 25 | private FriendlyIdDeserializer(boolean useFriendlyFormat) { 26 | super(UUID.class); 27 | this.useFriendlyFormat = useFriendlyFormat; 28 | } 29 | 30 | @Override 31 | public UUID deserialize(JsonParser parser, DeserializationContext ctxt) { 32 | var token = parser.currentToken(); 33 | if (token == JsonToken.VALUE_STRING) { 34 | var value = parser.getString().trim(); 35 | if (useFriendlyFormat) { 36 | return parse(value).toUuid(); 37 | } else { 38 | return UUID.fromString(value); 39 | } 40 | } 41 | throw ctxt.weirdStringException(parser.getString(), UUID.class, "Expected UUID string value"); 42 | } 43 | 44 | @Override 45 | public ValueDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { 46 | if (property != null) { 47 | var annotation = property.getAnnotation(IdFormat.class); 48 | if (annotation != null && annotation.value() == FriendlyIdFormat.RAW) { 49 | return new FriendlyIdDeserializer(false); 50 | } 51 | } 52 | return this; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /friendly-id/src/jmh/java/com/devskiller/friendly_id/FriendlyIdBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import java.util.UUID; 4 | 5 | import org.openjdk.jmh.annotations.Benchmark; 6 | import org.openjdk.jmh.annotations.Fork; 7 | import org.openjdk.jmh.annotations.Measurement; 8 | import org.openjdk.jmh.annotations.OperationsPerInvocation; 9 | import org.openjdk.jmh.annotations.Scope; 10 | import org.openjdk.jmh.annotations.Setup; 11 | import org.openjdk.jmh.annotations.State; 12 | import org.openjdk.jmh.annotations.Warmup; 13 | import org.openjdk.jmh.infra.Blackhole; 14 | import org.openjdk.jmh.runner.Runner; 15 | import org.openjdk.jmh.runner.RunnerException; 16 | import org.openjdk.jmh.runner.options.Options; 17 | import org.openjdk.jmh.runner.options.OptionsBuilder; 18 | 19 | @State(Scope.Benchmark) 20 | @Warmup(iterations = 3) 21 | @Measurement(iterations = 10) 22 | @Fork(2) 23 | public class FriendlyIdBenchmark { 24 | 25 | static final int SIZE = 1_000_000; 26 | 27 | UUID[] uuids; 28 | String[] ids; 29 | 30 | public static void main(String[] args) throws RunnerException { 31 | Options opt = new OptionsBuilder() 32 | .include(FriendlyIdBenchmark.class.getSimpleName()) 33 | .build(); 34 | 35 | new Runner(opt).run(); 36 | } 37 | 38 | @Setup 39 | public void setup() { 40 | uuids = new UUID[SIZE]; 41 | ids = new String[SIZE]; 42 | for (int i = 0; i < SIZE; i++) { 43 | uuids[i] = UUID.randomUUID(); 44 | ids[i] = FriendlyId.toFriendlyId(uuids[i]); 45 | } 46 | } 47 | 48 | @Benchmark 49 | @OperationsPerInvocation(SIZE) 50 | public void serializeUuid(Blackhole blackhole) { 51 | for (int i = 0; i < SIZE; i++) { 52 | blackhole.consume(FriendlyId.toFriendlyId(uuids[i])); 53 | } 54 | } 55 | 56 | @Benchmark 57 | @OperationsPerInvocation(SIZE) 58 | public void deserializeId(Blackhole blackhole) { 59 | for (int i = 0; i < SIZE; i++) { 60 | blackhole.consume(FriendlyId.toUuid(ids[i])); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/main/java/com/devskiller/friendly_id/jackson2/FriendlyIdAnnotationIntrospector.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jackson2; 2 | 3 | import java.io.Serial; 4 | import java.util.UUID; 5 | 6 | import com.fasterxml.jackson.databind.deser.std.UUIDDeserializer; 7 | import com.fasterxml.jackson.databind.introspect.Annotated; 8 | import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; 9 | import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; 10 | import com.fasterxml.jackson.databind.ser.std.UUIDSerializer; 11 | 12 | import com.devskiller.friendly_id.IdFormat; 13 | 14 | public class FriendlyIdAnnotationIntrospector extends JacksonAnnotationIntrospector { 15 | 16 | @Serial 17 | private static final long serialVersionUID = 1L; 18 | 19 | @Override 20 | public Object findSerializer(Annotated annotatedMethod) { 21 | IdFormat annotation = _findAnnotation(annotatedMethod, IdFormat.class); 22 | if (annotatedMethod.getRawType() == UUID.class) { 23 | if (annotation != null) { 24 | return switch (annotation.value()) { 25 | case RAW -> UUIDSerializer.class; 26 | case URL62 -> FriendlyIdSerializer.class; 27 | }; 28 | } 29 | return FriendlyIdSerializer.class; 30 | } else { 31 | return null; 32 | } 33 | } 34 | 35 | @Override 36 | public Object findDeserializer(Annotated annotatedMethod) { 37 | var annotation = _findAnnotation(annotatedMethod, IdFormat.class); 38 | if (rawDeserializationType(annotatedMethod) == UUID.class) { 39 | if (annotation != null) { 40 | return switch (annotation.value()) { 41 | case RAW -> UUIDDeserializer.class; 42 | case URL62 -> FriendlyIdDeserializer.class; 43 | }; 44 | } 45 | return FriendlyIdDeserializer.class; 46 | } 47 | return null; 48 | } 49 | 50 | private Class rawDeserializationType(Annotated a) { 51 | if (a instanceof AnnotatedMethod am && am.getParameterCount() == 1) { 52 | return am.getRawParameterType(0); 53 | } 54 | return a.getRawType(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | - FriendlyId value object type (`com.devskiller.friendly_id.type.FriendlyId`) as an alternative to raw UUID 12 | - JPA integration module (`friendly-id-jpa`) with automatic AttributeConverter 13 | - OpenFeign integration module (`friendly-id-openfeign`) for FriendlyId support in Feign clients 14 | - jOOQ integration module (`friendly-id-jooq`) for FriendlyId support in jOOQ 15 | - Spring Boot JPA demo application showcasing FriendlyId with JPA, REST API, and OpenFeign 16 | 17 | ### Changed 18 | - Upgraded from Java 8 to Java 21 19 | - Upgraded from Spring Boot 2.2.2 to 3.4.1 20 | - Upgraded from JUnit 4 to JUnit 5 21 | - Migrated from Vavr property testing to JUnit 5 `@RepeatedTest` 22 | - Updated Spring Boot auto-configuration to use `AutoConfiguration.imports` instead of `spring.factories` 23 | 24 | ### Fixed 25 | - Fixed Jackson module serialization by adding `super.setupModule(context)` call in `FriendlyIdModule` 26 | - Fixed Spring Cloud version compatibility (2024.0.0 for Spring Boot 3.4.1) 27 | - Added `friendly-id-jackson-datatype` as dependency to `friendly-id-spring-boot-starter` for complete auto-configuration 28 | - Added `friendly-id-jackson-datatype` as dependency to `friendly-id-openfeign` for JSON serialization support 29 | 30 | ### Dependencies 31 | - `friendly-id-spring-boot-starter` now includes `friendly-id-jackson-datatype` transitively 32 | - `friendly-id-openfeign` now includes `friendly-id-jackson-datatype` transitively 33 | 34 | ### Infrastructure 35 | - Migrated from legacy Sonatype OSSRH to Central Portal for Maven Central publishing 36 | - Updated `central-publishing-maven-plugin` to 0.6.0 for automated publishing 37 | 38 | ## [1.1.0] - Previous version 39 | - Legacy implementation with UUID-only support 40 | -------------------------------------------------------------------------------- /friendly-id/src/jmh/java/com/devskiller/friendly_id/UuidConverterBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import java.math.BigInteger; 4 | import java.util.Random; 5 | import java.util.UUID; 6 | 7 | import org.openjdk.jmh.annotations.Benchmark; 8 | import org.openjdk.jmh.annotations.Fork; 9 | import org.openjdk.jmh.annotations.Measurement; 10 | import org.openjdk.jmh.annotations.OperationsPerInvocation; 11 | import org.openjdk.jmh.annotations.Scope; 12 | import org.openjdk.jmh.annotations.Setup; 13 | import org.openjdk.jmh.annotations.State; 14 | import org.openjdk.jmh.annotations.Warmup; 15 | import org.openjdk.jmh.infra.Blackhole; 16 | import org.openjdk.jmh.runner.Runner; 17 | import org.openjdk.jmh.runner.RunnerException; 18 | import org.openjdk.jmh.runner.options.Options; 19 | import org.openjdk.jmh.runner.options.OptionsBuilder; 20 | 21 | @State(Scope.Benchmark) 22 | @Warmup(iterations = 3) 23 | @Measurement(iterations = 10) 24 | @Fork(2) 25 | public class UuidConverterBenchmark { 26 | 27 | static final int SIZE = 1_000_000; 28 | 29 | UUID[] uuids; 30 | BigInteger[] ids; 31 | 32 | public static void main(String[] args) throws RunnerException { 33 | Options opt = new OptionsBuilder() 34 | .include(UuidConverterBenchmark.class.getSimpleName()) 35 | .build(); 36 | new Runner(opt).run(); 37 | } 38 | 39 | @Setup 40 | public void setup() { 41 | uuids = new UUID[SIZE]; 42 | ids = new BigInteger[SIZE]; 43 | for (int i = 0; i < SIZE; i++) { 44 | uuids[i] = UUID.randomUUID(); 45 | ids[i] = new BigInteger(127, new Random()); 46 | } 47 | } 48 | 49 | @Benchmark 50 | @OperationsPerInvocation(SIZE) 51 | public void convertToBigInteger(Blackhole blackhole) { 52 | for (int i = 0; i < SIZE; i++) { 53 | blackhole.consume(UuidConverter.toBigInteger(uuids[i])); 54 | } 55 | } 56 | 57 | @Benchmark 58 | @OperationsPerInvocation(SIZE) 59 | public void convertFromBigInteger(Blackhole blackhole) { 60 | for (int i = 0; i < SIZE; i++) { 61 | blackhole.consume(UuidConverter.toUuid(ids[i])); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/src/main/java/com/devskiller/friendly_id/sample/hateos/FooController.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.hateos; 2 | 3 | import com.devskiller.friendly_id.sample.hateos.domain.Foo; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.hateoas.server.ExposesResourceFor; 6 | import org.springframework.http.HttpEntity; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.net.URI; 11 | import java.util.UUID; 12 | 13 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; 14 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; 15 | 16 | @Slf4j 17 | @RestController 18 | @ExposesResourceFor(FooResource.class) 19 | @RequestMapping("/foos") 20 | public class FooController { 21 | 22 | private final FooResourceAssembler assembler; 23 | 24 | public FooController(FooResourceAssembler assembler) { 25 | this.assembler = assembler; 26 | } 27 | 28 | @GetMapping("/{id}") 29 | public HttpEntity get(@PathVariable UUID id) { 30 | log.info("Get {}", id); 31 | Foo foo = new Foo(id, "Foo"); 32 | 33 | FooResource fooResource = assembler.toModel(foo); 34 | return ResponseEntity.ok(fooResource); 35 | } 36 | 37 | @PutMapping("/{id}") 38 | public HttpEntity update(@PathVariable UUID id, @RequestBody FooResource fooResource) { 39 | log.info("Update {} : {}", id, fooResource); 40 | Foo entity = new Foo(fooResource.getUuid(), fooResource.getName()); 41 | return ResponseEntity.ok(assembler.toModel(entity)); 42 | } 43 | 44 | @PostMapping 45 | public HttpEntity create(@RequestBody FooResource fooResource) { 46 | log.info("Create {}", fooResource.getUuid()); 47 | 48 | // Modern Spring HATEOAS 2.x - methodOn() triggers Spring's FriendlyId conversion 49 | URI location = linkTo(methodOn(FooController.class) 50 | .get(fooResource.getUuid())) 51 | .toUri(); 52 | 53 | return ResponseEntity.created(location).build(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/Product.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.jpa; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.Table; 9 | 10 | import com.devskiller.friendly_id.type.FriendlyId; 11 | 12 | /** 13 | * Product entity demonstrating FriendlyId usage with JPA. 14 | *

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

18 | */ 19 | @Entity 20 | @Table(name = "products") 21 | public class Product { 22 | 23 | @Id 24 | private FriendlyId id; 25 | 26 | @Column(nullable = false) 27 | private String name; 28 | 29 | @Column(length = 1000) 30 | private String description; 31 | 32 | @Column(nullable = false) 33 | private BigDecimal price; 34 | 35 | @Column(nullable = false) 36 | private Integer stock; 37 | 38 | // Default constructor required by JPA 39 | protected Product() { 40 | } 41 | 42 | public Product(String name, String description, BigDecimal price, Integer stock) { 43 | this.id = FriendlyId.random(); 44 | this.name = name; 45 | this.description = description; 46 | this.price = price; 47 | this.stock = stock; 48 | } 49 | 50 | // Getters and setters 51 | 52 | public FriendlyId getId() { 53 | return id; 54 | } 55 | 56 | public void setId(FriendlyId id) { 57 | this.id = id; 58 | } 59 | 60 | public String getName() { 61 | return name; 62 | } 63 | 64 | public void setName(String name) { 65 | this.name = name; 66 | } 67 | 68 | public String getDescription() { 69 | return description; 70 | } 71 | 72 | public void setDescription(String description) { 73 | this.description = description; 74 | } 75 | 76 | public BigDecimal getPrice() { 77 | return price; 78 | } 79 | 80 | public void setPrice(BigDecimal price) { 81 | this.price = price; 82 | } 83 | 84 | public Integer getStock() { 85 | return stock; 86 | } 87 | 88 | public void setStock(Integer stock) { 89 | this.stock = stock; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /friendly-id-jpa/src/test/java/com/devskiller/friendly_id/jpa/FriendlyIdConverterTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jpa; 2 | 3 | import java.util.UUID; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import com.devskiller.friendly_id.type.FriendlyId; 8 | 9 | import static com.devskiller.friendly_id.FriendlyIds.toFriendlyId; 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class FriendlyIdConverterTest { 13 | 14 | private final FriendlyIdConverter converter = new FriendlyIdConverter(); 15 | 16 | @Test 17 | void shouldConvertFriendlyIdToUuid() { 18 | // given 19 | FriendlyId friendlyId = FriendlyId.parse("5wbwf6yUxVBcr48AMbz9cb"); 20 | 21 | // when 22 | UUID uuid = converter.convertToDatabaseColumn(friendlyId); 23 | 24 | // then 25 | assertEquals(friendlyId.uuid(), uuid); 26 | } 27 | 28 | @Test 29 | void shouldConvertUuidToFriendlyId() { 30 | // given 31 | UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); 32 | 33 | // when 34 | FriendlyId friendlyId = converter.convertToEntityAttribute(uuid); 35 | 36 | // then 37 | assertEquals(uuid, friendlyId.uuid()); 38 | } 39 | 40 | @Test 41 | void shouldHandleNullFriendlyId() { 42 | // when 43 | UUID uuid = converter.convertToDatabaseColumn(null); 44 | 45 | // then 46 | assertNull(uuid); 47 | } 48 | 49 | @Test 50 | void shouldHandleNullUuid() { 51 | // when 52 | FriendlyId friendlyId = converter.convertToEntityAttribute(null); 53 | 54 | // then 55 | assertNull(friendlyId); 56 | } 57 | 58 | @Test 59 | void shouldBeReversible() { 60 | // given 61 | UUID originalUuid = UUID.randomUUID(); 62 | 63 | // when 64 | FriendlyId friendlyId = converter.convertToEntityAttribute(originalUuid); 65 | UUID convertedUuid = converter.convertToDatabaseColumn(friendlyId); 66 | 67 | // then 68 | assertEquals(originalUuid, convertedUuid); 69 | } 70 | 71 | @Test 72 | void shouldConvertToStringWhenNeeded() { 73 | // given 74 | UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); 75 | FriendlyId friendlyId = converter.convertToEntityAttribute(uuid); 76 | 77 | // when 78 | String friendlyIdString = friendlyId.toString(); 79 | 80 | // then 81 | assertEquals(toFriendlyId(uuid), friendlyIdString); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/src/test/java/com/devskiller/friendly_id/spring/FieldWithoutFriendlyIdTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import java.util.UUID; 4 | 5 | import tools.jackson.databind.json.JsonMapper; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static com.devskiller.friendly_id.spring.ObjectMapperConfiguration.mapper; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | class FieldWithoutFriendlyIdTest { 12 | 13 | private final UUID uuid = UUID.fromString("f088ce5b-9279-4cc3-946a-c15ad740dd6d"); 14 | private JsonMapper jsonMapper = mapper(); 15 | 16 | @Test 17 | void shouldAllowToDoNotCodeUuidInDataObject() { 18 | var foo = new Foo(uuid, uuid); 19 | 20 | var json = jsonMapper.writeValueAsString(foo); 21 | 22 | // JSON field order may vary, so check each field separately 23 | assertThat(json).contains("\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\""); 24 | assertThat(json).contains("\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\""); 25 | 26 | var cloned = jsonMapper.readValue(json, Foo.class); 27 | assertThat(cloned.rawUuid()).isEqualTo(foo.friendlyId()); 28 | } 29 | 30 | @Test 31 | void shouldDeserializeUuidsInDataObject() { 32 | var json = """ 33 | {"rawUuid":"f088ce5b-9279-4cc3-946a-c15ad740dd6d","friendlyId":"7Jsg6CPDscHawyJfE70b9x"}"""; 34 | 35 | var cloned = jsonMapper.readValue(json, Foo.class); 36 | assertThat(cloned.rawUuid()).isEqualTo(uuid); 37 | assertThat(cloned.friendlyId()).isEqualTo(uuid); 38 | } 39 | 40 | @Test 41 | void shouldSerializeUuidsInValueObject() { 42 | jsonMapper = mapper(); 43 | 44 | var bar = new Bar(uuid, uuid); 45 | 46 | var json = jsonMapper.writeValueAsString(bar); 47 | 48 | assertThat(json).isEqualToIgnoringWhitespace(""" 49 | {"rawUuid":"f088ce5b-9279-4cc3-946a-c15ad740dd6d","friendlyId":"7Jsg6CPDscHawyJfE70b9x"}"""); 50 | } 51 | 52 | @Test 53 | void shouldDeserializeUuidsInValueObject() { 54 | jsonMapper = mapper(); 55 | 56 | var json = """ 57 | {"rawUuid":"f088ce5b-9279-4cc3-946a-c15ad740dd6d","friendlyId":"7Jsg6CPDscHawyJfE70b9x"}"""; 58 | 59 | var deserialized = jsonMapper.readValue(json, Bar.class); 60 | 61 | assertThat(deserialized.rawUuid()).isEqualTo(uuid); 62 | assertThat(deserialized.friendlyId()).isEqualTo(uuid); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /friendly-id-samples/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | friendly-id-samples 6 | pom 7 | FriendlyId Samples 8 | Sample applications demonstrating FriendlyId usage 9 | 10 | 11 | com.devskiller.friendly-id 12 | friendly-id-project 13 | ${revision} 14 | .. 15 | 16 | 17 | 18 | friendly-id-spring-boot-simple 19 | friendly-id-spring-boot3-simple 20 | friendly-id-spring-boot-customized 21 | friendly-id-spring-boot-hateos 22 | friendly-id-spring-boot-jpa-demo 23 | friendly-id-contracts 24 | 25 | 26 | 27 | 28 | 29 | maven-install-plugin 30 | 31 | true 32 | 33 | 34 | 35 | maven-deploy-plugin 36 | 37 | true 38 | 39 | 40 | 41 | org.sonatype.central 42 | central-publishing-maven-plugin 43 | 44 | true 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.jacoco 52 | jacoco-maven-plugin 53 | 54 | true 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /friendly-id-openfeign/src/test/java/com/devskiller/friendly_id/openfeign/FriendlyIdEncoderTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.openfeign; 2 | 3 | import java.util.UUID; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import com.devskiller.friendly_id.type.FriendlyId; 8 | 9 | import feign.RequestTemplate; 10 | import feign.codec.Encoder; 11 | 12 | import static com.devskiller.friendly_id.FriendlyIds.toFriendlyId; 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | class FriendlyIdEncoderTest { 16 | 17 | @Test 18 | void shouldEncodeUuidAsFriendlyId() { 19 | // given 20 | UUID uuid = UUID.fromString("c3587ec5-0976-497f-8374-61e0c2ea3da5"); 21 | String expectedFriendlyId = toFriendlyId(uuid); 22 | 23 | String[] capturedValue = new String[1]; 24 | Encoder delegateEncoder = (object, bodyType, template) 25 | -> capturedValue[0] = (String) object; 26 | FriendlyIdEncoder encoder = new FriendlyIdEncoder(delegateEncoder); 27 | RequestTemplate template = new RequestTemplate(); 28 | 29 | // when 30 | encoder.encode(uuid, UUID.class, template); 31 | 32 | // then 33 | assertThat(capturedValue[0]).isEqualTo(expectedFriendlyId); 34 | } 35 | 36 | @Test 37 | void shouldEncodeFriendlyIdValueObjectAsString() { 38 | // given 39 | UUID uuid = UUID.fromString("c3587ec5-0976-497f-8374-61e0c2ea3da5"); 40 | FriendlyId friendlyId = FriendlyId.of(uuid); 41 | String expectedString = friendlyId.toString(); 42 | 43 | String[] capturedValue = new String[1]; 44 | Encoder delegateEncoder = (object, bodyType, template) 45 | -> capturedValue[0] = (String) object; 46 | FriendlyIdEncoder encoder = new FriendlyIdEncoder(delegateEncoder); 47 | RequestTemplate template = new RequestTemplate(); 48 | 49 | // when 50 | encoder.encode(friendlyId, FriendlyId.class, template); 51 | 52 | // then 53 | assertThat(capturedValue[0]).isEqualTo(expectedString); 54 | } 55 | 56 | @Test 57 | void shouldDelegateOtherTypes() { 58 | // given 59 | String regularString = "test"; 60 | 61 | String[] capturedValue = new String[1]; 62 | Encoder delegateEncoder = (object, bodyType, template) 63 | -> capturedValue[0] = (String) object; 64 | FriendlyIdEncoder encoder = new FriendlyIdEncoder(delegateEncoder); 65 | RequestTemplate template = new RequestTemplate(); 66 | 67 | // when 68 | encoder.encode(regularString, String.class, template); 69 | 70 | // then 71 | assertThat(capturedValue[0]).isEqualTo(regularString); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/src/test/java/com/devskiller/friendly_id/spring/FieldWithoutFriendlyIdTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.spring; 2 | 3 | import java.util.UUID; 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static com.devskiller.friendly_id.spring.ObjectMapperConfiguration.mapper; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | class FieldWithoutFriendlyIdTest { 13 | 14 | private final UUID uuid = UUID.fromString("f088ce5b-9279-4cc3-946a-c15ad740dd6d"); 15 | private ObjectMapper mapper = mapper(); 16 | 17 | @Test 18 | void shouldAllowToDoNotCodeUuidInDataObject() throws Exception { 19 | Foo foo = new Foo(); 20 | foo.setRawUuid(uuid); 21 | foo.setFriendlyId(uuid); 22 | 23 | String json = mapper.writeValueAsString(foo); 24 | 25 | assertThat(json).isEqualToIgnoringWhitespace( 26 | "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}" 27 | ); 28 | 29 | Foo cloned = mapper.readValue(json, Foo.class); 30 | assertThat(cloned.getRawUuid()).isEqualTo(foo.getFriendlyId()); 31 | } 32 | 33 | @Test 34 | void shouldDeserializeUuidsInDataObject() throws Exception { 35 | String json = "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}"; 36 | 37 | Foo cloned = mapper.readValue(json, Foo.class); 38 | assertThat(cloned.getRawUuid()).isEqualTo(uuid); 39 | assertThat(cloned.getFriendlyId()).isEqualTo(uuid); 40 | } 41 | 42 | 43 | @Test 44 | void shouldSerializeUuidsInValueObject() throws Exception { 45 | mapper = mapper(new ParameterNamesModule()); 46 | 47 | Bar bar = new Bar(uuid, uuid); 48 | 49 | String json = mapper.writeValueAsString(bar); 50 | 51 | assertThat(json).isEqualToIgnoringWhitespace( 52 | "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}" 53 | ); 54 | } 55 | 56 | @Test 57 | void shouldDeserializeUuuidsValueObject() throws Exception { 58 | mapper = mapper(new ParameterNamesModule()); 59 | 60 | String json = "{\"rawUuid\":\"f088ce5b-9279-4cc3-946a-c15ad740dd6d\",\"friendlyId\":\"7Jsg6CPDscHawyJfE70b9x\"}"; 61 | 62 | Bar deserialized = mapper.readValue(json, Bar.class); 63 | 64 | assertThat(deserialized.getRawUuid()).isEqualTo(uuid); 65 | assertThat(deserialized.getFriendlyId()).isEqualTo(uuid); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /friendly-id-jackson-datatype/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | friendly-id-project 7 | com.devskiller.friendly-id 8 | ${revision} 9 | .. 10 | 11 | 12 | friendly-id-jackson-datatype 13 | 14 | FriendlyId Jackson Datatype 15 | Jackson module for JSON serialization/deserialization of UUIDs as FriendlyIds 16 | 17 | 18 | 19 | com.devskiller.friendly-id 20 | friendly-id 21 | ${project.version} 22 | 23 | 24 | 25 | com.fasterxml.jackson.core 26 | jackson-annotations 27 | 28 | 29 | 30 | tools.jackson.core 31 | jackson-core 32 | 33 | 34 | tools.jackson.core 35 | jackson-databind 36 | 37 | 38 | 39 | org.junit.jupiter 40 | junit-jupiter 41 | test 42 | 43 | 44 | org.assertj 45 | assertj-core 46 | test 47 | 48 | 49 | 50 | 51 | 52 | 53 | org.apache.maven.plugins 54 | maven-compiler-plugin 55 | 56 | true 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /friendly-id-openfeign/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | friendly-id-project 7 | com.devskiller.friendly-id 8 | ${revision} 9 | .. 10 | 11 | 12 | friendly-id-openfeign 13 | 14 | FriendlyId OpenFeign Integration 15 | OpenFeign client integration for FriendlyId - enables automatic FriendlyId encoding/decoding in Feign clients 16 | 17 | 18 | 19 | com.devskiller.friendly-id 20 | friendly-id 21 | ${project.version} 22 | 23 | 24 | com.devskiller.friendly-id 25 | friendly-id-jackson-datatype 26 | ${project.version} 27 | 28 | 29 | org.springframework.cloud 30 | spring-cloud-starter-openfeign 31 | provided 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | provided 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-classic 42 | provided 43 | 44 | 45 | 46 | 47 | org.junit.jupiter 48 | junit-jupiter 49 | test 50 | 51 | 52 | org.assertj 53 | assertj-core 54 | test 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /friendly-id-openfeign/src/test/java/com/devskiller/friendly_id/openfeign/FriendlyIdDecoderTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.openfeign; 2 | 3 | import java.io.IOException; 4 | import java.util.UUID; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import com.devskiller.friendly_id.type.FriendlyId; 9 | 10 | import feign.codec.Decoder; 11 | 12 | import static com.devskiller.friendly_id.FriendlyIds.toFriendlyId; 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | class FriendlyIdDecoderTest { 16 | 17 | @Test 18 | void shouldDecodeFriendlyIdStringToUuid() throws IOException { 19 | // given 20 | UUID expectedUuid = UUID.fromString("c3587ec5-0976-497f-8374-61e0c2ea3da5"); 21 | String friendlyIdString = toFriendlyId(expectedUuid); 22 | 23 | Decoder delegateDecoder = (response, type) -> friendlyIdString; 24 | FriendlyIdDecoder decoder = new FriendlyIdDecoder(delegateDecoder); 25 | 26 | // when 27 | Object result = decoder.decode(null, UUID.class); 28 | 29 | // then 30 | assertThat(result).isEqualTo(expectedUuid); 31 | } 32 | 33 | @Test 34 | void shouldDecodeFriendlyIdStringToFriendlyIdValueObject() throws IOException { 35 | // given 36 | UUID uuid = UUID.fromString("c3587ec5-0976-497f-8374-61e0c2ea3da5"); 37 | String friendlyIdString = toFriendlyId(uuid); 38 | 39 | Decoder delegateDecoder = (response, type) -> friendlyIdString; 40 | FriendlyIdDecoder decoder = new FriendlyIdDecoder(delegateDecoder); 41 | 42 | // when 43 | Object result = decoder.decode(null, FriendlyId.class); 44 | 45 | // then 46 | assertThat(result) 47 | .isInstanceOf(FriendlyId.class) 48 | .extracting(fid -> ((FriendlyId) fid).uuid()) 49 | .isEqualTo(uuid); 50 | } 51 | 52 | @Test 53 | void shouldDelegateOtherTypes() throws IOException { 54 | // given 55 | String regularString = "test"; 56 | Decoder delegateDecoder = (response, type) -> regularString; 57 | FriendlyIdDecoder decoder = new FriendlyIdDecoder(delegateDecoder); 58 | 59 | // when 60 | Object result = decoder.decode(null, String.class); 61 | 62 | // then 63 | assertThat(result).isEqualTo(regularString); 64 | } 65 | 66 | @Test 67 | void shouldDelegateWhenResponseIsNotString() throws IOException { 68 | // given 69 | Integer number = 42; 70 | Decoder delegateDecoder = (response, type) -> number; 71 | FriendlyIdDecoder decoder = new FriendlyIdDecoder(delegateDecoder); 72 | 73 | // when 74 | Object result = decoder.decode(null, UUID.class); 75 | 76 | // then 77 | assertThat(result).isEqualTo(number); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /friendly-id-jooq/src/test/java/com/devskiller/friendly_id/jooq/FriendlyIdConverterTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jooq; 2 | 3 | import java.util.UUID; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import com.devskiller.friendly_id.type.FriendlyId; 8 | 9 | import static com.devskiller.friendly_id.FriendlyIds.toFriendlyId; 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class FriendlyIdConverterTest { 13 | 14 | private final FriendlyIdConverter converter = new FriendlyIdConverter(); 15 | 16 | @Test 17 | void shouldConvertUuidToFriendlyId() { 18 | // given 19 | UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); 20 | 21 | // when 22 | FriendlyId friendlyId = converter.from(uuid); 23 | 24 | // then 25 | assertEquals(uuid, friendlyId.uuid()); 26 | } 27 | 28 | @Test 29 | void shouldConvertFriendlyIdToUuid() { 30 | // given 31 | FriendlyId friendlyId = FriendlyId.parse("5wbwf6yUxVBcr48AMbz9cb"); 32 | 33 | // when 34 | UUID uuid = converter.to(friendlyId); 35 | 36 | // then 37 | assertEquals(friendlyId.uuid(), uuid); 38 | } 39 | 40 | @Test 41 | void shouldHandleNullUuid() { 42 | // when 43 | FriendlyId friendlyId = converter.from(null); 44 | 45 | // then 46 | assertNull(friendlyId); 47 | } 48 | 49 | @Test 50 | void shouldHandleNullFriendlyId() { 51 | // when 52 | UUID uuid = converter.to(null); 53 | 54 | // then 55 | assertNull(uuid); 56 | } 57 | 58 | @Test 59 | void shouldReturnCorrectFromType() { 60 | // when 61 | Class fromType = converter.fromType(); 62 | 63 | // then 64 | assertEquals(UUID.class, fromType); 65 | } 66 | 67 | @Test 68 | void shouldReturnCorrectToType() { 69 | // when 70 | Class toType = converter.toType(); 71 | 72 | // then 73 | assertEquals(FriendlyId.class, toType); 74 | } 75 | 76 | @Test 77 | void shouldBeReversible() { 78 | // given 79 | UUID originalUuid = UUID.randomUUID(); 80 | 81 | // when 82 | FriendlyId friendlyId = converter.from(originalUuid); 83 | UUID convertedUuid = converter.to(friendlyId); 84 | 85 | // then 86 | assertEquals(originalUuid, convertedUuid); 87 | } 88 | 89 | @Test 90 | void shouldConvertToStringWhenNeeded() { 91 | // given 92 | UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); 93 | FriendlyId friendlyId = converter.from(uuid); 94 | 95 | // when 96 | String friendlyIdString = friendlyId.toString(); 97 | 98 | // then 99 | assertEquals(toFriendlyId(uuid), friendlyIdString); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/src/test/java/com/devskiller/friendly_id/sample/hateos/FooControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.hateos; 2 | 3 | import com.devskiller.friendly_id.spring.EnableFriendlyId; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; 7 | import org.springframework.context.annotation.Import; 8 | import org.springframework.test.web.servlet.MockMvc; 9 | 10 | import static org.hamcrest.CoreMatchers.is; 11 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 12 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 13 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 16 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; 17 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 18 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 19 | 20 | @WebMvcTest 21 | @EnableFriendlyId // Required for UUID <-> FriendlyID conversion in path variables 22 | @Import({FooResourceAssembler.class, BarResourceAssembler.class}) // Import assemblers for WebMvcTest 23 | class FooControllerTest { 24 | 25 | @Autowired 26 | MockMvc mockMvc; 27 | 28 | @Test 29 | void shouldGet() throws Exception { 30 | mockMvc.perform(get("/foos/{id}", "cafe")) 31 | .andDo(print()) 32 | .andExpect(status().isOk()) 33 | .andExpect(content().contentType("application/hal+json")) 34 | .andExpect(jsonPath("$.uuid", is("cafe"))) 35 | .andExpect(jsonPath("$._links.self.href", is("http://localhost/foos/cafe"))); 36 | } 37 | 38 | @Test 39 | void shouldCreate() throws Exception { 40 | mockMvc.perform(post("/foos") 41 | .content("{\"uuid\":\"newFoo\",\"name\":\"Very New Foo\"}") 42 | .contentType("application/hal+json")) 43 | .andDo(print()) 44 | .andExpect(header().string("Location", "http://localhost/foos/newFoo")) 45 | .andExpect(status().isCreated()); 46 | } 47 | 48 | @Test 49 | void update() throws Exception { 50 | mockMvc.perform(put("/foos/{id}", "foo") 51 | .content("{\"uuid\":\"foo\",\"name\":\"Sample Foo\"}") 52 | .contentType("application/hal+json")) 53 | .andDo(print()) 54 | .andExpect(status().isOk()); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIds.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import java.util.Objects; 4 | import java.util.UUID; 5 | 6 | /** 7 | * Utility class to convert between UUID and FriendlyId strings. 8 | *

9 | * FriendlyId is a URL-friendly Base62 encoding of UUID that produces 10 | * shorter strings (up to 22 characters) compared to standard UUID format (36 characters). 11 | *

12 | * 13 | *

Usage Example

14 | *
{@code
15 |  * // Create random FriendlyId
16 |  * String id = FriendlyIds.createFriendlyId();
17 |  *
18 |  * // Convert UUID to FriendlyId
19 |  * String friendlyId = FriendlyIds.toFriendlyId(uuid);
20 |  *
21 |  * // Convert FriendlyId to UUID
22 |  * UUID uuid = FriendlyIds.toUuid(friendlyId);
23 |  * }
24 | * 25 | * @since 2.0 26 | * @see com.devskiller.friendly_id.type.FriendlyId 27 | */ 28 | public final class FriendlyIds { 29 | 30 | private FriendlyIds() { 31 | // utility class 32 | } 33 | 34 | /** 35 | * Creates a random FriendlyId string. 36 | * 37 | * @return FriendlyId encoded random UUID 38 | */ 39 | public static String createFriendlyId() { 40 | return Url62.encode(UUID.randomUUID()); 41 | } 42 | 43 | /** 44 | * Encodes a UUID to FriendlyId string. 45 | * 46 | * @param uuid UUID to be encoded, must not be null 47 | * @return FriendlyId encoded UUID 48 | * @throws NullPointerException if uuid is null 49 | */ 50 | public static String toFriendlyId(UUID uuid) { 51 | Objects.requireNonNull(uuid, "UUID cannot be null"); 52 | return Url62.encode(uuid); 53 | } 54 | 55 | /** 56 | * Converts a string to UUID, accepting both UUID and FriendlyId formats. 57 | *

58 | * This method auto-detects the format: 59 | *

    60 | *
  • Standard UUID format (36 chars with hyphens): parsed directly
  • 61 | *
  • FriendlyId format (up to 22 chars): decoded from Base62
  • 62 | *
63 | * 64 | * @param value UUID or FriendlyId string, must not be null 65 | * @return parsed UUID 66 | * @throws NullPointerException if value is null 67 | * @throws IllegalArgumentException if value is not a valid UUID or FriendlyId 68 | */ 69 | public static UUID toUuid(String value) { 70 | Objects.requireNonNull(value, "Value cannot be null"); 71 | if (isStandardUuidFormat(value)) { 72 | return UUID.fromString(value); 73 | } 74 | return Url62.decode(value); 75 | } 76 | 77 | private static boolean isStandardUuidFormat(String value) { 78 | return value.length() == 36 79 | && value.charAt(8) == '-' 80 | && value.charAt(13) == '-' 81 | && value.charAt(18) == '-' 82 | && value.charAt(23) == '-'; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /friendly-id-openfeign/src/main/java/com/devskiller/friendly_id/openfeign/FriendlyIdConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.openfeign; 2 | 3 | import org.springframework.beans.factory.ObjectFactory; 4 | import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters; 5 | import org.springframework.cloud.openfeign.support.SpringDecoder; 6 | import org.springframework.cloud.openfeign.support.SpringEncoder; 7 | import org.springframework.context.annotation.Bean; 8 | 9 | import feign.codec.Decoder; 10 | import feign.codec.Encoder; 11 | 12 | /** 13 | * Configuration for FriendlyId integration with Spring Cloud OpenFeign. 14 | *

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

17 | *
{@code
18 |  * @FeignClient(name = "user-service", configuration = FriendlyIdConfiguration.class)
19 |  * public interface UserClient {
20 |  *
21 |  *     @GetMapping("/users/{id}")
22 |  *     UserDto getUser(@PathVariable UUID id);
23 |  *
24 |  *     @GetMapping("/users/{id}/profile")
25 |  *     ProfileDto getProfile(@PathVariable FriendlyId id);
26 |  * }
27 |  * }
28 | *

29 | * The configuration registers custom encoder and decoder that: 30 | *

31 | *
    32 | *
  • Convert UUID/FriendlyId to FriendlyId strings in request URLs/bodies
  • 33 | *
  • Convert FriendlyId strings back to UUID/FriendlyId in responses
  • 34 | *
35 | * 36 | * @since 1.1.1 37 | */ 38 | @SuppressWarnings("deprecation") 39 | public class FriendlyIdConfiguration { 40 | 41 | /** 42 | * Creates a FriendlyId-aware Feign encoder. 43 | * The encoder delegates to SpringEncoder for actual encoding but intercepts 44 | * UUID and FriendlyId objects to convert them to FriendlyId strings. 45 | */ 46 | @Bean 47 | @SuppressWarnings({"unchecked", "rawtypes"}) 48 | public Encoder feignEncoder(ObjectFactory messageConverters) { 49 | // Cast needed due to API compatibility between Spring Boot 4 and Spring Cloud OpenFeign 50 | return new FriendlyIdEncoder(new SpringEncoder((ObjectFactory) messageConverters)); 51 | } 52 | 53 | /** 54 | * Creates a FriendlyId-aware Feign decoder. 55 | * The decoder delegates to SpringDecoder for actual decoding but converts 56 | * FriendlyId strings back to UUID or FriendlyId objects when needed. 57 | */ 58 | @Bean 59 | @SuppressWarnings({"unchecked", "rawtypes"}) 60 | public Decoder feignDecoder(ObjectFactory messageConverters) { 61 | // Cast needed due to API compatibility between Spring Boot 4 and Spring Cloud OpenFeign 62 | return new FriendlyIdDecoder(new SpringDecoder((ObjectFactory) messageConverters)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot3-simple/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | com.devskiller.friendly-id 7 | spring-boot3-simple 8 | ${revision} 9 | 10 | FriendlyId Spring Boot 3 Sample 11 | Sample application demonstrating FriendlyId with Spring Boot 3 and Jackson 2.x 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-parent 16 | 3.4.1 17 | 18 | 19 | 20 | 21 | 2.0.0-SNAPSHOT 22 | UTF-8 23 | UTF-8 24 | 21 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-web 31 | 32 | 33 | com.devskiller.friendly-id 34 | friendly-id-jackson2-datatype 35 | ${project.version} 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-test 41 | test 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-maven-plugin 50 | 51 | 52 | maven-compiler-plugin 53 | 3.14.0 54 | 55 | true 56 | 57 | 58 | 59 | org.apache.maven.plugins 60 | maven-deploy-plugin 61 | 62 | true 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-simple/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | com.devskiller.friendly-id 7 | spring-boot-simple 8 | ${revision} 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 4.0.1 14 | 15 | 16 | 17 | 18 | 2.0.0-SNAPSHOT 19 | UTF-8 20 | UTF-8 21 | 21 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | com.devskiller.friendly-id 31 | friendly-id-spring-boot-starter 32 | ${project.version} 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-test 38 | test 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-webmvc-test 43 | test 44 | 45 | 46 | 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-maven-plugin 52 | 53 | 54 | maven-compiler-plugin 55 | 3.14.0 56 | 57 | true 58 | 59 | 60 | 61 | org.apache.maven.plugins 62 | maven-deploy-plugin 63 | 64 | true 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /friendly-id-jackson2-datatype/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | friendly-id-project 7 | com.devskiller.friendly-id 8 | ${revision} 9 | .. 10 | 11 | 12 | friendly-id-jackson2-datatype 13 | 14 | FriendlyId Jackson 2.x Datatype 15 | Jackson 2.x module for JSON serialization/deserialization of UUIDs as FriendlyIds 16 | 17 | 18 | 2.18.2 19 | 20 | 21 | 22 | 23 | com.devskiller.friendly-id 24 | friendly-id 25 | ${project.version} 26 | 27 | 28 | com.fasterxml.jackson.core 29 | jackson-annotations 30 | ${jackson2.version} 31 | 32 | 33 | com.fasterxml.jackson.core 34 | jackson-core 35 | ${jackson2.version} 36 | 37 | 38 | com.fasterxml.jackson.core 39 | jackson-databind 40 | ${jackson2.version} 41 | 42 | 43 | com.fasterxml.jackson.module 44 | jackson-module-parameter-names 45 | ${jackson2.version} 46 | 47 | 48 | 49 | org.junit.jupiter 50 | junit-jupiter 51 | test 52 | 53 | 54 | org.assertj 55 | assertj-core 56 | test 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.apache.maven.plugins 64 | maven-compiler-plugin 65 | 66 | true 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot3-simple/src/test/java/com/devskiller/friendly_id/sample/spring3/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.spring3; 2 | 3 | import java.util.UUID; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.test.web.servlet.MockMvc; 11 | 12 | import com.devskiller.friendly_id.FriendlyIds; 13 | 14 | import static org.hamcrest.Matchers.is; 15 | import static org.hamcrest.Matchers.notNullValue; 16 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 17 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 18 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 19 | 20 | @SpringBootTest 21 | @AutoConfigureMockMvc 22 | class ApplicationTest { 23 | 24 | @Autowired 25 | private MockMvc mockMvc; 26 | 27 | @Test 28 | void shouldSerializeAllIdFormats() throws Exception { 29 | // given 30 | UUID uuid = UUID.randomUUID(); 31 | String friendlyId = FriendlyIds.toFriendlyId(uuid); 32 | String rawUuid = uuid.toString(); 33 | 34 | // when/then 35 | mockMvc.perform(get("/items/{id}", friendlyId) 36 | .accept(MediaType.APPLICATION_JSON)) 37 | .andExpect(status().isOk()) 38 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 39 | .andExpect(jsonPath("$.id", is(friendlyId))) 40 | .andExpect(jsonPath("$.rawId", is(rawUuid))) 41 | .andExpect(jsonPath("$.friendlyUuid", is(friendlyId))) 42 | .andExpect(jsonPath("$.friendlyId", is(friendlyId))); 43 | } 44 | 45 | @Test 46 | void shouldDeserializeAndSerialize() throws Exception { 47 | // given 48 | UUID uuid = UUID.randomUUID(); 49 | String friendlyId = FriendlyIds.toFriendlyId(uuid); 50 | String json = """ 51 | {"id": "%s"} 52 | """.formatted(friendlyId); 53 | 54 | // when/then 55 | mockMvc.perform(post("/items") 56 | .contentType(MediaType.APPLICATION_JSON) 57 | .content(json) 58 | .accept(MediaType.APPLICATION_JSON)) 59 | .andExpect(status().isOk()) 60 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 61 | .andExpect(jsonPath("$.id", is(friendlyId))); 62 | } 63 | 64 | @Test 65 | void shouldGenerateAllIdsWhenNotProvided() throws Exception { 66 | // given 67 | String json = "{}"; 68 | 69 | // when/then 70 | mockMvc.perform(post("/items") 71 | .contentType(MediaType.APPLICATION_JSON) 72 | .content(json) 73 | .accept(MediaType.APPLICATION_JSON)) 74 | .andExpect(status().isOk()) 75 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 76 | .andExpect(jsonPath("$.id", notNullValue())) 77 | .andExpect(jsonPath("$.rawId", notNullValue())) 78 | .andExpect(jsonPath("$.friendlyUuid", notNullValue())) 79 | .andExpect(jsonPath("$.friendlyId", notNullValue())); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /friendly-id/src/main/java/com/devskiller/friendly_id/Base62.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id; 2 | 3 | import java.math.BigInteger; 4 | import java.util.function.BiFunction; 5 | import java.util.regex.Pattern; 6 | import java.util.stream.IntStream; 7 | 8 | import static java.util.Objects.requireNonNull; 9 | 10 | /** 11 | * Base62 encoder/decoder. 12 | *

13 | * This is free and unencumbered public domain software 14 | *

15 | * Source: https://github.com/opencoinage/opencoinage/blob/master/src/java/org/opencoinage/util/Base62.java 16 | */ 17 | class Base62 { 18 | 19 | private static final BigInteger BASE = BigInteger.valueOf(62); 20 | private static final String DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 21 | 22 | /** 23 | * Encodes a number using Base62 encoding. 24 | * 25 | * @param number a positive integer 26 | * @return a Base62 string 27 | * 28 | * @throws IllegalArgumentException if number is a negative integer 29 | */ 30 | static String encode(BigInteger number) { 31 | if (number.compareTo(BigInteger.ZERO) < 0) { 32 | throwIllegalArgumentException("number must not be negative"); 33 | } 34 | StringBuilder result = new StringBuilder(); 35 | while (number.compareTo(BigInteger.ZERO) > 0) { 36 | BigInteger[] divmod = number.divideAndRemainder(BASE); 37 | number = divmod[0]; 38 | int digit = divmod[1].intValue(); 39 | result.insert(0, DIGITS.charAt(digit)); 40 | } 41 | return (result.isEmpty()) ? DIGITS.substring(0, 1) : result.toString(); 42 | } 43 | 44 | private static BigInteger throwIllegalArgumentException(String format, Object... args) { 45 | throw new IllegalArgumentException(String.format(format, args)); 46 | } 47 | 48 | /** 49 | * Decodes a string using Base62 encoding. 50 | * 51 | * @param string a Base62 string 52 | * @return a positive integer 53 | * 54 | * @throws IllegalArgumentException if string is empty 55 | */ 56 | static BigInteger decode(final String string) { 57 | return decode(string, 128); 58 | } 59 | 60 | static BigInteger decode(final String string, int bitLimit) { 61 | requireNonNull(string, "Decoded string must not be null"); 62 | if (string.isEmpty()) { 63 | return throwIllegalArgumentException("String '%s' must not be empty", string); 64 | } 65 | 66 | if (!Pattern.matches("[" + DIGITS + "]*", string)) { 67 | throwIllegalArgumentException("String '%s' contains illegal characters, only '%s' are allowed", string, DIGITS); 68 | } 69 | 70 | return IntStream.range(0, string.length()) 71 | .mapToObj(index -> BigInteger.valueOf(charAt.apply(string, index)).multiply(BASE.pow(index))) 72 | .reduce(BigInteger.ZERO, (acc, value) -> { 73 | BigInteger sum = acc.add(value); 74 | if (bitLimit > 0 && sum.bitLength() > bitLimit) { 75 | throwIllegalArgumentException("String '%s' contains more than 128bit information", string); 76 | } 77 | return sum; 78 | }); 79 | 80 | } 81 | 82 | private static final BiFunction charAt = (string, index) -> 83 | DIGITS.indexOf(string.charAt(string.length() - index - 1)); 84 | 85 | } -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/FriendlyIdJpaDemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.jpa; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.boot.CommandLineRunner; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | /** 11 | * Spring Boot application demonstrating FriendlyId usage with JPA. 12 | *

13 | * This demo shows: 14 | *

15 | *
    16 | *
  • Using FriendlyId as entity ID (stored as UUID in database)
  • 17 | *
  • Automatic conversion in REST endpoints via @PathVariable
  • 18 | *
  • JSON serialization with FriendlyId strings
  • 19 | *
  • H2 console for database inspection
  • 20 | *
21 | *

22 | * Access points: 23 | *

24 | *
    25 | *
  • REST API: http://localhost:8080/api/products
  • 26 | *
  • H2 Console: http://localhost:8080/h2-console (JDBC URL: jdbc:h2:mem:friendlyid_demo)
  • 27 | *
28 | */ 29 | @SpringBootApplication 30 | public class FriendlyIdJpaDemoApplication { 31 | 32 | public static void main(String[] args) { 33 | SpringApplication.run(FriendlyIdJpaDemoApplication.class, args); 34 | } 35 | 36 | /** 37 | * Initialize database with sample data. 38 | */ 39 | @Bean 40 | public CommandLineRunner initData(ProductRepository repository) { 41 | return args -> { 42 | System.out.println(""" 43 | 44 | ======================================== 45 | Initializing demo products... 46 | ======================================== 47 | """); 48 | 49 | var laptop = new Product( 50 | "Laptop", 51 | "High-performance laptop for developers", 52 | new BigDecimal("1299.99"), 53 | 15 54 | ); 55 | repository.save(laptop); 56 | System.out.println("Created product: Laptop with ID: " + laptop.getId()); 57 | 58 | var mouse = new Product( 59 | "Wireless Mouse", 60 | "Ergonomic wireless mouse", 61 | new BigDecimal("29.99"), 62 | 50 63 | ); 64 | repository.save(mouse); 65 | System.out.println("Created product: Wireless Mouse with ID: " + mouse.getId()); 66 | 67 | var keyboard = new Product( 68 | "Mechanical Keyboard", 69 | "RGB mechanical keyboard with Cherry MX switches", 70 | new BigDecimal("149.99"), 71 | 25 72 | ); 73 | repository.save(keyboard); 74 | System.out.println("Created product: Mechanical Keyboard with ID: " + keyboard.getId()); 75 | 76 | System.out.printf(""" 77 | 78 | ======================================== 79 | Demo ready! 80 | ======================================== 81 | REST API: http://localhost:8080/api/products 82 | H2 Console: http://localhost:8080/h2-console 83 | JDBC URL: jdbc:h2:mem:friendlyid_demo 84 | Username: sa 85 | Password: (empty) 86 | ======================================== 87 | 88 | Try these commands: 89 | curl http://localhost:8080/api/products 90 | curl http://localhost:8080/api/products/%s 91 | 92 | %n""", laptop.getId()); 93 | }; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/test/java/com/devskiller/friendly_id/sample/jpa/ProductClientIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.jpa; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | 13 | import com.devskiller.friendly_id.type.FriendlyId; 14 | 15 | import static org.hamcrest.Matchers.*; 16 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 17 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 18 | 19 | /** 20 | * Integration test demonstrating FriendlyId usage with JPA. 21 | *

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

24 | *
    25 | *
  1. Entity stored in database with FriendlyId as UUID
  2. 26 | *
  3. REST controller accepts FriendlyId in @PathVariable
  4. 27 | *
  5. JSON serialization converts FriendlyId to/from string
  6. 28 | *
29 | */ 30 | @SpringBootTest 31 | @AutoConfigureMockMvc 32 | class ProductClientIntegrationTest { 33 | 34 | @Autowired 35 | private ProductRepository repository; 36 | 37 | @Autowired 38 | private MockMvc mockMvc; 39 | 40 | private Product testProduct; 41 | 42 | @BeforeEach 43 | void setUp() { 44 | repository.deleteAll(); 45 | 46 | testProduct = new Product( 47 | "Test Product", 48 | "Product for integration testing", 49 | new BigDecimal("99.99"), 50 | 10 51 | ); 52 | repository.save(testProduct); 53 | } 54 | 55 | @Test 56 | void shouldRetrieveAllProducts() throws Exception { 57 | mockMvc.perform(get("/api/products") 58 | .accept(MediaType.APPLICATION_JSON)) 59 | .andExpect(status().isOk()) 60 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 61 | .andExpect(jsonPath("$", hasSize(1))) 62 | .andExpect(jsonPath("$[0].name", is("Test Product"))); 63 | } 64 | 65 | @Test 66 | void shouldRetrieveProductByFriendlyId() throws Exception { 67 | FriendlyId productId = testProduct.getId(); 68 | 69 | mockMvc.perform(get("/api/products/{id}", productId.toString()) 70 | .accept(MediaType.APPLICATION_JSON)) 71 | .andExpect(status().isOk()) 72 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 73 | .andExpect(jsonPath("$.id", is(productId.toString()))) 74 | .andExpect(jsonPath("$.name", is("Test Product"))) 75 | .andExpect(jsonPath("$.description", is("Product for integration testing"))) 76 | .andExpect(jsonPath("$.price", is(99.99))) 77 | .andExpect(jsonPath("$.stock", is(10))); 78 | } 79 | 80 | @Test 81 | void shouldHandleFriendlyIdConversionInUrlPath() throws Exception { 82 | // This test verifies that FriendlyId in @PathVariable is correctly: 83 | // 1. Parsed from string by Spring MVC converter 84 | // 2. Used to query the database via JPA converter 85 | // 3. Serialized to JSON string in response 86 | 87 | mockMvc.perform(get("/api/products/{id}", testProduct.getId().toString()) 88 | .accept(MediaType.APPLICATION_JSON)) 89 | .andExpect(status().isOk()) 90 | .andExpect(jsonPath("$.id", matchesPattern("[0-9A-Za-z]{21,22}"))); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-jpa-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.devskiller.friendly-id 6 | spring-boot-jpa-demo 7 | ${revision} 8 | 9 | 10 | org.springframework.boot 11 | spring-boot-starter-parent 12 | 4.0.1 13 | 14 | 15 | 16 | 17 | 2.0.0-SNAPSHOT 18 | UTF-8 19 | UTF-8 20 | 21 21 | 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-data-jpa 34 | 35 | 36 | 37 | 38 | com.h2database 39 | h2 40 | runtime 41 | 42 | 43 | 44 | 45 | com.devskiller.friendly-id 46 | friendly-id-spring-boot-starter 47 | ${project.version} 48 | 49 | 50 | 51 | 52 | com.devskiller.friendly-id 53 | friendly-id-jpa 54 | ${project.version} 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-test 61 | test 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-webmvc-test 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-maven-plugin 75 | 76 | 77 | maven-compiler-plugin 78 | 3.14.0 79 | 80 | true 81 | 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-deploy-plugin 86 | 87 | true 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /friendly-id-jpa/src/main/java/com/devskiller/friendly_id/jpa/FriendlyIdConverter.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jpa; 2 | 3 | import java.util.UUID; 4 | 5 | import jakarta.persistence.AttributeConverter; 6 | import jakarta.persistence.Converter; 7 | 8 | import com.devskiller.friendly_id.type.FriendlyId; 9 | 10 | /** 11 | * JPA AttributeConverter for transparent UUID to FriendlyId conversion in entity mappings. 12 | *

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

17 | *

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

22 | * 23 | *

Automatic Registration (JPA 2.1+)

24 | *

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

28 | * 29 | *

Usage in Entity

30 | *
{@code
 31 |  * @Entity
 32 |  * public class User {
 33 |  *     @Id
 34 |  *     private FriendlyId id;
 35 |  *
 36 |  *     private String name;
 37 |  *
 38 |  *     // getters/setters
 39 |  * }
 40 |  * }
41 | * 42 | *

Query Examples

43 | *
{@code
 44 |  * // JPQL - use UUID parameter
 45 |  * FriendlyId userId = FriendlyId.parse("5wbwf6yUxVBcr48AMbz9cb");
 46 |  * User user = em.createQuery("SELECT u FROM User u WHERE u.id = :id", User.class)
 47 |  *     .setParameter("id", userId)
 48 |  *     .getSingleResult();
 49 |  *
 50 |  * // Criteria API
 51 |  * CriteriaBuilder cb = em.getCriteriaBuilder();
 52 |  * CriteriaQuery query = cb.createQuery(User.class);
 53 |  * Root root = query.from(User.class);
 54 |  * query.where(cb.equal(root.get("id"), userId));
 55 |  *
 56 |  * // Native query - use UUID
 57 |  * User user = em.createNativeQuery(
 58 |  *         "SELECT * FROM users WHERE id = ?",
 59 |  *         User.class)
 60 |  *     .setParameter(1, userId.uuid())
 61 |  *     .getSingleResult();
 62 |  * }
63 | * 64 | *

Manual Application (Optional)

65 | *

66 | * If autoApply is disabled or you need explicit control: 67 | *

68 | *
{@code
 69 |  * @Entity
 70 |  * public class User {
 71 |  *     @Id
 72 |  *     @Convert(converter = FriendlyIdConverter.class)
 73 |  *     private FriendlyId id;
 74 |  * }
 75 |  * }
76 | * 77 | * @see FriendlyId 78 | * @see jakarta.persistence.AttributeConverter 79 | * @since 1.1.1 80 | */ 81 | @Converter(autoApply = true) 82 | public class FriendlyIdConverter implements AttributeConverter { 83 | 84 | /** 85 | * Converts a FriendlyId value object to a database UUID. 86 | * 87 | * @param attribute the FriendlyId value object, may be {@code null} 88 | * @return the UUID for database storage, or {@code null} if input is {@code null} 89 | */ 90 | @Override 91 | public UUID convertToDatabaseColumn(FriendlyId attribute) { 92 | return attribute == null ? null : attribute.uuid(); 93 | } 94 | 95 | /** 96 | * Converts a database UUID to a FriendlyId value object. 97 | * 98 | * @param dbData the UUID from the database, may be {@code null} 99 | * @return the FriendlyId value object, or {@code null} if input is {@code null} 100 | */ 101 | @Override 102 | public FriendlyId convertToEntityAttribute(UUID dbData) { 103 | return dbData == null ? null : FriendlyId.of(dbData); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /friendly-id-jooq/src/main/java/com/devskiller/friendly_id/jooq/FriendlyIdConverter.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.jooq; 2 | 3 | import java.io.Serial; 4 | import java.util.UUID; 5 | 6 | import org.jooq.Converter; 7 | 8 | import com.devskiller.friendly_id.type.FriendlyId; 9 | 10 | /** 11 | * jOOQ converter for transparent UUID to FriendlyId conversion in database queries. 12 | *

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

17 | *

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

22 | * 23 | *

Usage with jOOQ Code Generator

24 | *

25 | * Configure this converter in your jOOQ code generation configuration to apply it 26 | * to specific columns or all UUID columns: 27 | *

28 | *
{@code
 29 |  * 
 30 |  *   
 31 |  *     
 32 |  *       
 33 |  *         
 34 |  *           com.devskiller.friendly_id.type.FriendlyId
 35 |  *           com.devskiller.friendly_id.jooq.FriendlyIdConverter
 36 |  *           .*\.ID
 37 |  *           UUID
 38 |  *         
 39 |  *       
 40 |  *     
 41 |  *   
 42 |  * 
 43 |  * }
44 | * 45 | *

Manual Usage Example

46 | *
{@code
 47 |  * // Query using FriendlyId
 48 |  * FriendlyId friendlyId = FriendlyId.parse("5wbwf6yUxVBcr48AMbz9cb");
 49 |  * UserRecord user = create
 50 |  *     .selectFrom(USER)
 51 |  *     .where(USER.ID.eq(friendlyId))
 52 |  *     .fetchOne();
 53 |  *
 54 |  * // Get FriendlyId from result
 55 |  * FriendlyId userId = user.getId(); // Returns FriendlyId value object
 56 |  * String friendlyIdString = userId.toString(); // Get string when needed
 57 |  *
 58 |  * // Insert with FriendlyId
 59 |  * create.insertInto(USER)
 60 |  *     .set(USER.ID, FriendlyId.random())
 61 |  *     .set(USER.NAME, "John Doe")
 62 |  *     .execute();
 63 |  * }
64 | * 65 | * @see FriendlyId 66 | * @see org.jooq.Converter 67 | */ 68 | public class FriendlyIdConverter implements Converter { 69 | 70 | @Serial 71 | private static final long serialVersionUID = 1L; 72 | 73 | /** 74 | * Converts a database UUID to a FriendlyId value object. 75 | * 76 | * @param databaseObject the UUID from the database, may be {@code null} 77 | * @return the FriendlyId value object, or {@code null} if input is {@code null} 78 | */ 79 | @Override 80 | public FriendlyId from(UUID databaseObject) { 81 | return databaseObject == null ? null : FriendlyId.of(databaseObject); 82 | } 83 | 84 | /** 85 | * Converts a FriendlyId value object to a database UUID. 86 | * 87 | * @param userObject the FriendlyId value object, may be {@code null} 88 | * @return the UUID representation, or {@code null} if input is {@code null} 89 | */ 90 | @Override 91 | public UUID to(FriendlyId userObject) { 92 | return userObject == null ? null : userObject.uuid(); 93 | } 94 | 95 | /** 96 | * Returns the database type (UUID). 97 | * 98 | * @return {@code UUID.class} 99 | */ 100 | @Override 101 | public Class fromType() { 102 | return UUID.class; 103 | } 104 | 105 | /** 106 | * Returns the user type (FriendlyId). 107 | * 108 | * @return {@code FriendlyId.class} 109 | */ 110 | @Override 111 | public Class toType() { 112 | return FriendlyId.class; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-customized/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | com.devskiller.friendly-id 7 | spring-boot-customized 8 | ${revision} 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 4.0.1 14 | 15 | 16 | 17 | 18 | 2.0.0-SNAPSHOT 19 | UTF-8 20 | UTF-8 21 | 21 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | com.devskiller.friendly-id 31 | friendly-id-spring-boot-starter 32 | ${project.version} 33 | 34 | 35 | 36 | 37 | 38 | org.projectlombok 39 | lombok 40 | true 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-test 46 | test 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-webmvc-test 51 | test 52 | 53 | 54 | 55 | 56 | 57 | 58 | org.apache.maven.plugins 59 | maven-compiler-plugin 60 | 61 | true 62 | 63 | 64 | org.projectlombok 65 | lombok 66 | 67 | 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-maven-plugin 73 | 74 | 75 | 76 | org.projectlombok 77 | lombok 78 | 79 | 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-surefire-plugin 85 | 86 | -XX:+EnableDynamicAgentLoading 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-deploy-plugin 92 | 93 | true 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /friendly-id-jooq/README.md: -------------------------------------------------------------------------------- 1 | # FriendlyId jOOQ Integration 2 | 3 | jOOQ converter for transparent UUID to FriendlyId conversion in database queries. 4 | 5 | ## Overview 6 | 7 | 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. 8 | 9 | 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). 10 | 11 | ## Maven Dependency 12 | 13 | ```xml 14 | 15 | com.devskiller.friendly-id 16 | friendly-id-jooq 17 | 1.1.1-SNAPSHOT 18 | 19 | ``` 20 | 21 | ## Usage with jOOQ Code Generator 22 | 23 | Configure the converter in your jOOQ code generation configuration to apply it to specific columns or all UUID columns: 24 | 25 | ```xml 26 | 27 | 28 | 29 | 30 | 31 | com.devskiller.friendly_id.type.FriendlyId 32 | com.devskiller.friendly_id.jooq.FriendlyIdConverter 33 | .*\.ID 34 | UUID 35 | 36 | 37 | 38 | 39 | 40 | ``` 41 | 42 | ## Example 43 | 44 | ```java 45 | import com.devskiller.friendly_id.type.FriendlyId; 46 | 47 | // Query using FriendlyId 48 | FriendlyId friendlyId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); 49 | UserRecord user = create 50 | .selectFrom(USER) 51 | .where(USER.ID.eq(friendlyId)) // Automatically converted to UUID for database 52 | .fetchOne(); 53 | 54 | // Get FriendlyId from result 55 | FriendlyId userId = user.getId(); // Returns FriendlyId value object 56 | String userIdString = userId.toString(); // Get string representation when needed 57 | 58 | // Insert with FriendlyId 59 | create.insertInto(USER) 60 | .set(USER.ID, FriendlyId.random()) 61 | .set(USER.NAME, "John Doe") 62 | .execute(); 63 | 64 | // FriendlyId prints nicely 65 | System.out.println("User ID: " + userId); // Prints: User ID: 5wbwf6yUxVBcr48AMbz9cb 66 | ``` 67 | 68 | ## How It Works 69 | 70 | The `FriendlyIdConverter` implements `org.jooq.Converter`: 71 | 72 | - **Database Type (fromType)**: `UUID` - the actual column type in your database (16 bytes) 73 | - **User Type (toType)**: `FriendlyId` - value object wrapping UUID in your Java code (~28 bytes) 74 | - **Conversion**: Bidirectional conversion between UUID and FriendlyId value objects 75 | 76 | ## Memory Efficiency 77 | 78 | | Type | Memory Usage | Notes | 79 | |------|-------------|-------| 80 | | UUID | 16 bytes | Database storage | 81 | | FriendlyId | ~28 bytes | 16 bytes UUID + ~12 bytes object header | 82 | | String | ~40-50 bytes | FriendlyId as String (previous approach) | 83 | 84 | **Result**: ~30-40% memory savings compared to storing FriendlyId as String 85 | 86 | ## Benefits 87 | 88 | - **Memory Efficient**: Store UUIDs internally, compute strings only when needed 89 | - **URL-Friendly**: Automatic conversion to human-readable, Base62-encoded IDs 90 | - **Type Safety**: Strong typing prevents mixing UUIDs with FriendlyIds 91 | - **Database Efficiency**: Store compact UUIDs in the database 92 | - **Transparent**: No manual conversion needed in your application code 93 | - **Pretty Printing**: Automatic FriendlyId string representation via `toString()` 94 | 95 | ## See Also 96 | 97 | - [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/) 98 | - [FriendlyId Core Library](../friendly-id/) 99 | - [FriendlyId Value Object](../friendly-id/src/main/java/com/devskiller/friendly_id/type/FriendlyId.java) 100 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-hateos/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | com.devskiller.friendly-id 7 | spring-boot-hateos 8 | ${revision} 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 4.0.1 14 | 15 | 16 | 17 | 18 | 2.0.0-SNAPSHOT 19 | UTF-8 20 | UTF-8 21 | 21 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | com.devskiller.friendly-id 31 | friendly-id-spring-boot-starter 32 | ${project.version} 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-hateoas 37 | 38 | 39 | org.atteo 40 | evo-inflector 41 | 1.3 42 | 43 | 44 | 45 | 46 | org.projectlombok 47 | lombok 48 | true 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-test 54 | test 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-webmvc-test 59 | test 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.apache.maven.plugins 67 | maven-compiler-plugin 68 | 69 | true 70 | 71 | 72 | org.projectlombok 73 | lombok 74 | 75 | 76 | 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-maven-plugin 81 | 82 | 83 | 84 | org.projectlombok 85 | lombok 86 | 87 | 88 | 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-deploy-plugin 93 | 94 | true 95 | 96 | 97 | 98 | org.jacoco 99 | jacoco-maven-plugin 100 | 0.8.3 101 | 102 | true 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /PUBLISHING.md: -------------------------------------------------------------------------------- 1 | # Publishing to Maven Central 2 | 3 | This document describes how to publish `friendly-id` artifacts to Maven Central using the new Sonatype Central Portal. 4 | 5 | ## Prerequisites 6 | 7 | 1. **Account on Central Portal**: https://central.sonatype.com 8 | 2. **Namespace verified**: `com.devskiller.friendly-id` 9 | 3. **GPG Key**: For signing artifacts 10 | 4. **Maven credentials**: Token configured in `~/.m2/settings.xml` 11 | 12 | ## Configuration 13 | 14 | ### 1. Maven Settings (`~/.m2/settings.xml`) 15 | 16 | Add your Central Portal credentials: 17 | 18 | ```xml 19 | 20 | 21 | central 22 | YOUR_USERNAME 23 | YOUR_TOKEN 24 | 25 | 26 | ``` 27 | 28 | ### 2. GPG Key Setup 29 | 30 | Generate GPG key if you don't have one: 31 | 32 | ```bash 33 | # Generate key 34 | gpg --gen-key 35 | 36 | # List keys 37 | gpg --list-keys 38 | 39 | # Export public key to keyserver 40 | gpg --keyserver keyserver.ubuntu.com --send-keys YOUR_KEY_ID 41 | ``` 42 | 43 | ## Publishing Process 44 | 45 | ### Option 1: Snapshot Release (for testing) 46 | 47 | ```bash 48 | # Build and deploy snapshot 49 | mvn clean deploy 50 | ``` 51 | 52 | Snapshots will be available at: 53 | ``` 54 | https://central.sonatype.com/artifact/com.devskiller.friendly-id/friendly-id/1.1.1-SNAPSHOT 55 | ``` 56 | 57 | ### Option 2: Release Version 58 | 59 | 1. **Update version** (remove `-SNAPSHOT` suffix): 60 | ```bash 61 | # Update version in pom.xml 62 | mvn versions:set -DnewVersion=1.1.1 63 | mvn versions:commit 64 | ``` 65 | 66 | 2. **Build and deploy with release profile**: 67 | ```bash 68 | # This will: 69 | # - Create source JARs 70 | # - Create javadoc JARs 71 | # - Sign all artifacts with GPG 72 | # - Deploy to Central Portal 73 | mvn clean deploy -Prelease 74 | ``` 75 | 76 | 3. **Verify deployment**: 77 | - Go to https://central.sonatype.com/publishing/deployments 78 | - Check deployment status 79 | - Artifacts will be automatically published to Maven Central (autoPublish=true) 80 | 81 | 4. **Tag the release**: 82 | ```bash 83 | git tag -a 1.1.1 -m "Release version 1.1.1" 84 | git push origin 1.1.1 85 | ``` 86 | 87 | 5. **Prepare next development version**: 88 | ```bash 89 | mvn versions:set -DnewVersion=1.1.2-SNAPSHOT 90 | mvn versions:commit 91 | git add pom.xml */pom.xml 92 | git commit -m "chore: prepare next development version 1.1.2-SNAPSHOT" 93 | git push 94 | ``` 95 | 96 | ## Troubleshooting 97 | 98 | ### GPG Signing Issues 99 | 100 | If you get "gpg: signing failed: Inappropriate ioctl for device": 101 | ```bash 102 | export GPG_TTY=$(tty) 103 | ``` 104 | 105 | Or add to `~/.bashrc`: 106 | ```bash 107 | export GPG_TTY=$(tty) 108 | ``` 109 | 110 | ### Wrong credentials 111 | 112 | Make sure `central` in settings.xml matches `central` in pom.xml. 113 | 114 | ### Deployment verification 115 | 116 | Check deployment status: 117 | ```bash 118 | # List recent deployments 119 | curl -u "YOUR_USERNAME:YOUR_TOKEN" \ 120 | https://central.sonatype.com/api/v1/publisher/deployments 121 | ``` 122 | 123 | ## Maven Central Sync 124 | 125 | After successful deployment: 126 | - Artifacts are **immediately available** on Central Portal 127 | - Sync to Maven Central (repo1.maven.org) takes **10-30 minutes** 128 | - Search index update (search.maven.org) takes **up to 2 hours** 129 | 130 | ## Verification 131 | 132 | After publication, verify artifacts are available: 133 | 134 | ```bash 135 | # Check on Central Portal 136 | curl https://central.sonatype.com/artifact/com.devskiller.friendly-id/friendly-id/1.1.1 137 | 138 | # Check on Maven Central (after sync) 139 | curl https://repo1.maven.org/maven2/com/devskiller/friendly-id/friendly-id/1.1.1/ 140 | ``` 141 | 142 | ## CI/CD Integration (Future) 143 | 144 | For automated releases via GitHub Actions, you'll need to: 145 | 146 | 1. Add GitHub Secrets: 147 | - `MAVEN_CENTRAL_USERNAME` 148 | - `MAVEN_CENTRAL_TOKEN` 149 | - `GPG_PRIVATE_KEY` 150 | - `GPG_PASSPHRASE` 151 | 152 | 2. Create `.github/workflows/release.yml` workflow 153 | 154 | 3. Use `central-publishing-maven-plugin` in the workflow 155 | 156 | ## References 157 | 158 | - [Central Portal Documentation](https://central.sonatype.org/publish/publish-portal-maven/) 159 | - [Requirements](https://central.sonatype.org/publish/requirements/) 160 | - [Central Publishing Maven Plugin](https://central.sonatype.org/publish/publish-portal-maven/) 161 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-customized/src/test/java/com/devskiller/friendly_id/sample/customized/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.customized; 2 | 3 | import java.util.UUID; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.test.context.bean.override.mockito.MockitoBean; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | 13 | import com.devskiller.friendly_id.spring.EnableFriendlyId; 14 | 15 | import static com.devskiller.friendly_id.FriendlyIds.toFriendlyId; 16 | import static com.devskiller.friendly_id.FriendlyIds.toUuid; 17 | import static org.hamcrest.CoreMatchers.is; 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.ArgumentMatchers.eq; 20 | import static org.mockito.BDDMockito.given; 21 | import static org.mockito.BDDMockito.then; 22 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 23 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 27 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 28 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 29 | 30 | @WebMvcTest(ItemController.class) 31 | @EnableFriendlyId 32 | class ApplicationTest { 33 | 34 | @Autowired 35 | MockMvc mockMvc; 36 | 37 | @MockitoBean 38 | ItemService itemService; 39 | 40 | @Test 41 | void shouldSerializeAllIdFormats() throws Exception { 42 | // given 43 | UUID uuid = UUID.randomUUID(); 44 | String friendlyId = toFriendlyId(uuid); 45 | var item = new Item(uuid, uuid, uuid, com.devskiller.friendly_id.type.FriendlyId.of(uuid)); 46 | given(itemService.find(uuid)).willReturn(item); 47 | 48 | // expect 49 | mockMvc.perform(get("/items/{id}", friendlyId) 50 | .accept(MediaType.APPLICATION_JSON)) 51 | .andDo(print()) 52 | .andExpect(status().isOk()) 53 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 54 | .andExpect(jsonPath("$.id", is(friendlyId))) 55 | .andExpect(jsonPath("$.rawId", is(uuid.toString()))) 56 | .andExpect(jsonPath("$.friendlyUuid", is(friendlyId))) 57 | .andExpect(jsonPath("$.friendlyId", is(friendlyId))); 58 | } 59 | 60 | @Test 61 | void shouldDeserializeAndCreate() throws Exception { 62 | // given 63 | UUID uuid = UUID.randomUUID(); 64 | String friendlyId = toFriendlyId(uuid); 65 | String json = """ 66 | {"id": "%s", "rawId": "%s", "friendlyUuid": "%s", "friendlyId": "%s"} 67 | """.formatted(friendlyId, uuid, friendlyId, friendlyId); 68 | 69 | var item = new Item(uuid, uuid, uuid, com.devskiller.friendly_id.type.FriendlyId.of(uuid)); 70 | given(itemService.create(any(Item.class))).willReturn(item); 71 | 72 | // when 73 | mockMvc.perform(post("/items") 74 | .content(json) 75 | .contentType(MediaType.APPLICATION_JSON)) 76 | .andDo(print()) 77 | .andExpect(status().isOk()); 78 | 79 | // then 80 | then(itemService).should().create(any(Item.class)); 81 | } 82 | 83 | @Test 84 | void shouldDeserializeAndUpdate() throws Exception { 85 | // given 86 | UUID uuid = UUID.randomUUID(); 87 | String friendlyId = toFriendlyId(uuid); 88 | String json = """ 89 | {"id": "%s"} 90 | """.formatted(friendlyId); 91 | 92 | // when 93 | mockMvc.perform(put("/items/{id}", friendlyId) 94 | .content(json) 95 | .contentType(MediaType.APPLICATION_JSON)) 96 | .andDo(print()) 97 | .andExpect(status().isOk()); 98 | 99 | // then 100 | then(itemService).should().update(eq(uuid), any(Item.class)); 101 | } 102 | 103 | @Test 104 | void shouldWorkWithPseudoUuid() throws Exception { 105 | // given 106 | UUID itemId = toUuid("itemId"); 107 | String friendlyId = toFriendlyId(itemId); 108 | var item = new Item(itemId, itemId, itemId, com.devskiller.friendly_id.type.FriendlyId.of(itemId)); 109 | given(itemService.find(itemId)).willReturn(item); 110 | 111 | // expect 112 | mockMvc.perform(get("/items/{id}", "itemId") 113 | .accept(MediaType.APPLICATION_JSON)) 114 | .andDo(print()) 115 | .andExpect(status().isOk()) 116 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 117 | .andExpect(jsonPath("$.id", is(friendlyId))) 118 | .andExpect(jsonPath("$.rawId", is(itemId.toString()))); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing Guide 2 | 3 | This document describes how to release a new version of FriendlyID to Maven Central. 4 | 5 | ## Prerequisites 6 | 7 | Before releasing, ensure you have: 8 | 9 | 1. **GitHub Repository Access**: Write access to push tags 10 | 2. **GitHub Secrets Configured**: The following secrets must be set in repository settings: 11 | - `OSSRH_USERNAME` - Sonatype OSSRH username 12 | - `OSSRH_TOKEN` - Sonatype OSSRH token/password 13 | - `GPG_PRIVATE_KEY` - GPG private key for artifact signing 14 | - `GPG_PASSPHRASE` - Passphrase for the GPG key 15 | 16 | ## Release Process 17 | 18 | ### 1. Prepare for Release 19 | 20 | Ensure your local repository is up to date and all tests pass: 21 | 22 | ```bash 23 | git checkout master 24 | git pull origin master 25 | mvn clean install 26 | ``` 27 | 28 | ### 2. Create Release Tag 29 | 30 | Create an annotated tag with the version number (must start with `v`): 31 | 32 | ```bash 33 | # For release version (e.g., 1.2.0) 34 | git tag -a v1.2.0 -m "Release 1.2.0" 35 | 36 | # For release candidate (e.g., 1.2.0-RC1) 37 | git tag -a v1.2.0-RC1 -m "Release 1.2.0-RC1" 38 | ``` 39 | 40 | ### 3. Push Tag to GitHub 41 | 42 | Push the tag to trigger the automated release: 43 | 44 | ```bash 45 | git push origin v1.2.0 46 | ``` 47 | 48 | ### 4. Monitor Release Process 49 | 50 | 1. Go to **Actions** tab in GitHub repository 51 | 2. Watch the **Release** workflow execution 52 | 3. The workflow will: 53 | - Build all modules with the specified version 54 | - Run all tests (can be skipped with `-DskipTests`) 55 | - Sign artifacts with GPG 56 | - Deploy to Maven Central (OSSRH) 57 | - Create GitHub Release with artifacts 58 | 59 | ### 5. Verify Release 60 | 61 | After successful deployment: 62 | 63 | 1. Check [Maven Central Repository](https://repo1.maven.org/maven2/com/devskiller/friendly-id/) 64 | 2. Verify the GitHub Release was created with artifacts 65 | 3. Test the release in a separate project: 66 | 67 | ```xml 68 | 69 | com.devskiller.friendly-id 70 | friendly-id 71 | 1.2.0 72 | 73 | ``` 74 | 75 | ## Version Numbering 76 | 77 | Follow [Semantic Versioning](https://semver.org/): 78 | 79 | - **MAJOR** version (X.0.0): Incompatible API changes 80 | - **MINOR** version (0.X.0): New functionality, backwards-compatible 81 | - **PATCH** version (0.0.X): Backwards-compatible bug fixes 82 | 83 | Examples: 84 | - `v1.0.0` - Initial release 85 | - `v1.1.0` - New feature (e.g., OpenFeign integration) 86 | - `v1.1.1` - Bug fix 87 | - `v2.0.0` - Breaking change (e.g., Java version upgrade) 88 | 89 | ## Snapshot Releases 90 | 91 | 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. 92 | 93 | ## Manual Release (Emergency) 94 | 95 | If automated release fails, you can release manually: 96 | 97 | ```bash 98 | # Build and deploy to Maven Central 99 | mvn clean deploy -P release -Drevision=1.2.0 100 | 101 | # Create GitHub release manually through GitHub UI 102 | ``` 103 | 104 | ## Rollback 105 | 106 | If a release needs to be rolled back: 107 | 108 | 1. **Do NOT delete tags from Maven Central** - versions are immutable 109 | 2. Delete the GitHub tag and release: 110 | ```bash 111 | git tag -d v1.2.0 112 | git push origin :refs/tags/v1.2.0 113 | ``` 114 | 3. Release a new patch version with the fix 115 | 116 | ## Troubleshooting 117 | 118 | ### GPG Signing Fails 119 | 120 | - Verify `GPG_PRIVATE_KEY` secret is correctly formatted (including `-----BEGIN PGP PRIVATE KEY BLOCK-----`) 121 | - Check `GPG_PASSPHRASE` is correct 122 | - Ensure GPG key hasn't expired 123 | 124 | ### Maven Central Deployment Fails 125 | 126 | - Verify `OSSRH_USERNAME` and `OSSRH_TOKEN` are correct 127 | - Check [OSSRH Status](https://status.maven.org/) 128 | - Review deployment logs in GitHub Actions 129 | 130 | ### Build Fails 131 | 132 | - Check all tests pass locally: `mvn clean install` 133 | - Review GitHub Actions logs for specific error 134 | - Ensure all dependencies are available in Maven Central 135 | 136 | ## Post-Release Tasks 137 | 138 | After successful release: 139 | 140 | 1. Update `` in parent `pom.xml` to next SNAPSHOT version 141 | 2. Update version in `README.md` examples (if needed) 142 | 3. Announce release (GitHub Discussions, Twitter, etc.) 143 | 4. Close related GitHub issues/PRs 144 | 145 | ## CI-Friendly Versioning 146 | 147 | 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: 148 | 149 | - **Default**: Defined in parent `pom.xml` (`1.1.1-SNAPSHOT`) 150 | - **Override**: `mvn -Drevision=X.Y.Z` 151 | - **Release**: GitHub Actions sets version from git tag 152 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-jpa-demo/src/main/java/com/devskiller/friendly_id/sample/jpa/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.jpa; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.DeleteMapping; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.PutMapping; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | import com.devskiller.friendly_id.type.FriendlyId; 17 | 18 | /** 19 | * REST controller demonstrating FriendlyId usage with Spring MVC and JPA. 20 | *

21 | * Key features demonstrated: 22 | *

23 | *
    24 | *
  • @PathVariable automatically converts FriendlyId string to FriendlyId value object
  • 25 | *
  • Response JSON contains FriendlyId as Base62 string (thanks to Jackson integration)
  • 26 | *
  • Database stores UUID internally (thanks to JPA converter)
  • 27 | *
28 | *

29 | * Example URLs: 30 | *

31 | *
 32 |  * GET  /api/products                    - List all products
 33 |  * GET  /api/products/5wbwf6yUxVBcr48   - Get product by FriendlyId
 34 |  * POST /api/products                    - Create new product
 35 |  * PUT  /api/products/5wbwf6yUxVBcr48   - Update product
 36 |  * DELETE /api/products/5wbwf6yUxVBcr48 - Delete product
 37 |  * 
38 | */ 39 | @RestController 40 | @RequestMapping("/api/products") 41 | public class ProductController { 42 | 43 | private final ProductRepository productRepository; 44 | 45 | public ProductController(ProductRepository productRepository) { 46 | this.productRepository = productRepository; 47 | } 48 | 49 | /** 50 | * Get all products. 51 | *

52 | * Response: JSON array with FriendlyId strings instead of UUIDs. 53 | *

54 | */ 55 | @GetMapping 56 | public List getAllProducts() { 57 | return productRepository.findAll(); 58 | } 59 | 60 | /** 61 | * Get product by FriendlyId. 62 | *

63 | * Example: GET /api/products/5wbwf6yUxVBcr48AMbz9cb 64 | *

65 | *

66 | * The FriendlyId string from URL is automatically converted to FriendlyId value object 67 | * by Spring's StringToFriendlyIdConverter. 68 | *

69 | */ 70 | @GetMapping("/{id}") 71 | public ResponseEntity getProductById(@PathVariable FriendlyId id) { 72 | return productRepository.findById(id) 73 | .map(ResponseEntity::ok) 74 | .orElse(ResponseEntity.notFound().build()); 75 | } 76 | 77 | /** 78 | * Create new product. 79 | *

80 | * Request body should NOT include 'id' - it will be generated automatically. 81 | *

82 | *

83 | * Example request: 84 | *

85 | *
 86 | 	 * {
 87 | 	 *   "name": "Laptop",
 88 | 	 *   "description": "High-performance laptop",
 89 | 	 *   "price": 1299.99,
 90 | 	 *   "stock": 10
 91 | 	 * }
 92 | 	 * 
93 | */ 94 | @PostMapping 95 | public ResponseEntity createProduct(@RequestBody ProductRequest request) { 96 | Product product = new Product( 97 | request.name(), 98 | request.description(), 99 | request.price(), 100 | request.stock() 101 | ); 102 | Product savedProduct = productRepository.save(product); 103 | return ResponseEntity.status(HttpStatus.CREATED).body(savedProduct); 104 | } 105 | 106 | /** 107 | * Update existing product. 108 | *

109 | * Example: PUT /api/products/5wbwf6yUxVBcr48AMbz9cb 110 | *

111 | */ 112 | @PutMapping("/{id}") 113 | public ResponseEntity updateProduct( 114 | @PathVariable FriendlyId id, 115 | @RequestBody ProductRequest request) { 116 | 117 | return productRepository.findById(id) 118 | .map(existingProduct -> { 119 | existingProduct.setName(request.name()); 120 | existingProduct.setDescription(request.description()); 121 | existingProduct.setPrice(request.price()); 122 | existingProduct.setStock(request.stock()); 123 | Product updatedProduct = productRepository.save(existingProduct); 124 | return ResponseEntity.ok(updatedProduct); 125 | }) 126 | .orElse(ResponseEntity.notFound().build()); 127 | } 128 | 129 | /** 130 | * Delete product by FriendlyId. 131 | *

132 | * Example: DELETE /api/products/5wbwf6yUxVBcr48AMbz9cb 133 | *

134 | */ 135 | @DeleteMapping("/{id}") 136 | public ResponseEntity deleteProduct(@PathVariable FriendlyId id) { 137 | if (productRepository.existsById(id)) { 138 | productRepository.deleteById(id); 139 | return ResponseEntity.noContent().build(); 140 | } 141 | return ResponseEntity.notFound().build(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /friendly-id/src/main/java/com/devskiller/friendly_id/type/FriendlyId.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.type; 2 | 3 | import java.io.Serial; 4 | import java.io.Serializable; 5 | import java.util.Objects; 6 | import java.util.UUID; 7 | 8 | import com.devskiller.friendly_id.FriendlyIds; 9 | 10 | /** 11 | * Value object representing a FriendlyId that wraps a UUID. 12 | *

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

17 | *

18 | * This type is designed to be used with: 19 | *

20 | *
    21 | *
  • jOOQ converters for database mapping
  • 22 | *
  • JPA AttributeConverters for entity mapping
  • 23 | *
  • Jackson serializers/deserializers for JSON
  • 24 | *
  • Spring MVC converters for request parameters
  • 25 | *
26 | * 27 | *

Memory Efficiency

28 | *

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

32 | *
    33 | *
  • UUID: 16 bytes
  • 34 | *
  • FriendlyId object: ~28 bytes (16 bytes UUID + ~12 bytes object header)
  • 35 | *
  • String: ~40-50 bytes (depending on FriendlyId length)
  • 36 | *
37 | * 38 | *

Usage Example

39 | *
{@code
 40 |  * // Create from UUID
 41 |  * UUID uuid = UUID.randomUUID();
 42 |  * FriendlyId id = FriendlyId.of(uuid);
 43 |  *
 44 |  * // Create from String
 45 |  * FriendlyId id = FriendlyId.parse("5wbwf6yUxVBcr48AMbz9cb");
 46 |  *
 47 |  * // Create random
 48 |  * FriendlyId id = FriendlyId.random();
 49 |  *
 50 |  * // Static import friendly
 51 |  * import static com.devskiller.friendly_id.type.FriendlyId.friendlyId;
 52 |  * FriendlyId id = friendlyId(uuid);
 53 |  *
 54 |  * // Get UUID
 55 |  * UUID uuid = id.toUuid();
 56 |  *
 57 |  * // Get FriendlyId string
 58 |  * String friendlyIdString = id.value();
 59 |  * }
60 | * 61 | * @since 2.0 62 | */ 63 | public final class FriendlyId implements Serializable, Comparable { 64 | 65 | @Serial 66 | private static final long serialVersionUID = 1L; 67 | 68 | private final UUID uuid; 69 | 70 | private FriendlyId(UUID uuid) { 71 | this.uuid = Objects.requireNonNull(uuid, "UUID cannot be null"); 72 | } 73 | 74 | /** 75 | * Creates a FriendlyId from a UUID. 76 | * 77 | * @param uuid the UUID to wrap, must not be null 78 | * @return a new FriendlyId instance 79 | * @throws NullPointerException if uuid is null 80 | */ 81 | public static FriendlyId of(UUID uuid) { 82 | return new FriendlyId(uuid); 83 | } 84 | 85 | /** 86 | * Creates a FriendlyId from a UUID. 87 | *

88 | * This method is designed for static imports: 89 | *

{@code
 90 | 	 * import static com.devskiller.friendly_id.type.FriendlyId.friendlyId;
 91 | 	 * FriendlyId id = friendlyId(uuid);
 92 | 	 * }
93 | * 94 | * @param uuid the UUID to wrap, must not be null 95 | * @return a new FriendlyId instance 96 | * @throws NullPointerException if uuid is null 97 | */ 98 | public static FriendlyId friendlyId(UUID uuid) { 99 | return new FriendlyId(uuid); 100 | } 101 | 102 | /** 103 | * Parses a string representation to FriendlyId. 104 | *

105 | * Accepts both FriendlyId format (e.g., "5wbwf6yUxVBcr48AMbz9cb") and 106 | * standard UUID format (e.g., "c3587ec5-0976-497f-8374-61e0c2ea3da5"). 107 | * 108 | * @param value the FriendlyId or UUID string to parse, must not be null 109 | * @return a new FriendlyId instance 110 | * @throws NullPointerException if value is null 111 | * @throws IllegalArgumentException if value is not a valid FriendlyId or UUID 112 | */ 113 | public static FriendlyId parse(String value) { 114 | Objects.requireNonNull(value, "Value cannot be null"); 115 | return new FriendlyId(FriendlyIds.toUuid(value)); 116 | } 117 | 118 | /** 119 | * Creates a random FriendlyId. 120 | * 121 | * @return a new random FriendlyId instance 122 | */ 123 | public static FriendlyId random() { 124 | return new FriendlyId(UUID.randomUUID()); 125 | } 126 | 127 | /** 128 | * Returns the underlying UUID. 129 | * 130 | * @return the UUID 131 | */ 132 | public UUID toUuid() { 133 | return uuid; 134 | } 135 | 136 | /** 137 | * Returns the underlying UUID. 138 | *

139 | * This is an alias for {@link #toUuid()} following record-style naming. 140 | * 141 | * @return the UUID 142 | */ 143 | public UUID uuid() { 144 | return uuid; 145 | } 146 | 147 | /** 148 | * Returns the FriendlyId string representation. 149 | *

150 | * The string is computed on demand from the internal UUID. 151 | * 152 | * @return the FriendlyId string 153 | */ 154 | public String value() { 155 | return FriendlyIds.toFriendlyId(uuid); 156 | } 157 | 158 | /** 159 | * Returns the FriendlyId string representation. 160 | *

161 | * The string is computed on demand from the internal UUID. 162 | *

163 | * 164 | * @return the FriendlyId string 165 | */ 166 | @Override 167 | public String toString() { 168 | return value(); 169 | } 170 | 171 | @Override 172 | public boolean equals(Object o) { 173 | return this == o || (o instanceof FriendlyId that && uuid.equals(that.uuid)); 174 | } 175 | 176 | @Override 177 | public int hashCode() { 178 | return uuid.hashCode(); 179 | } 180 | 181 | @Override 182 | public int compareTo(FriendlyId other) { 183 | return this.uuid.compareTo(other.uuid); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /friendly-id/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.devskiller.friendly-id 8 | friendly-id-project 9 | ${revision} 10 | .. 11 | 12 | 13 | friendly-id 14 | 15 | FriendlyId Core 16 | Core library for converting UUIDs to URL-friendly Base62-encoded IDs 17 | 18 | 19 | 20 | org.jspecify 21 | jspecify 22 | 1.0.0 23 | 24 | 25 | org.junit.jupiter 26 | junit-jupiter 27 | test 28 | 29 | 30 | org.assertj 31 | assertj-core 32 | test 33 | 34 | 35 | 36 | 37 | 38 | jmh 39 | 40 | 41 | 42 | org.codehaus.mojo 43 | build-helper-maven-plugin 44 | 3.0.0 45 | 46 | 47 | 48 | add-test-source 49 | add-test-resource 50 | 51 | 52 | 53 | src/jmh/java 54 | 55 | 56 | 57 | src/jmh/resources 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-compiler-plugin 67 | 68 | 69 | 70 | testCompile 71 | 72 | 73 | 74 | 75 | org.openjdk.jmh 76 | jmh-generator-annprocess 77 | ${jmh.version} 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | org.codehaus.mojo 86 | exec-maven-plugin 87 | 3.5.1 88 | 89 | 90 | run-benchmarks 91 | integration-test 92 | 93 | exec 94 | 95 | 96 | test 97 | java 98 | 99 | -classpath 100 | 101 | org.openjdk.jmh.Main 102 | .* 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | org.openjdk.jmh 113 | jmh-core 114 | 1.37 115 | test 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /friendly-id-samples/friendly-id-spring-boot-simple/src/test/java/com/devskiller/friendly_id/sample/simple/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.sample.simple; 2 | 3 | import java.util.UUID; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.test.web.servlet.MockMvc; 11 | 12 | import com.devskiller.friendly_id.FriendlyIds; 13 | 14 | import static org.hamcrest.Matchers.is; 15 | import static org.hamcrest.Matchers.notNullValue; 16 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 17 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 18 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 19 | 20 | @SpringBootTest 21 | @AutoConfigureMockMvc 22 | class ApplicationTest { 23 | 24 | @Autowired 25 | private MockMvc mockMvc; 26 | 27 | @Test 28 | void shouldAcceptFriendlyIdAsPathVariable() throws Exception { 29 | // given 30 | UUID uuid = UUID.randomUUID(); 31 | String friendlyId = FriendlyIds.toFriendlyId(uuid); 32 | String rawUuid = uuid.toString(); 33 | 34 | // when/then 35 | mockMvc.perform(get("/items/{id}", friendlyId) 36 | .accept(MediaType.APPLICATION_JSON)) 37 | .andExpect(status().isOk()) 38 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 39 | .andExpect(jsonPath("$.id", is(friendlyId))) 40 | .andExpect(jsonPath("$.rawId", is(rawUuid))) 41 | .andExpect(jsonPath("$.friendlyUuid", is(friendlyId))) 42 | .andExpect(jsonPath("$.friendlyId", is(friendlyId))); 43 | } 44 | 45 | @Test 46 | void shouldAcceptUuidAsPathVariable() throws Exception { 47 | // given 48 | UUID uuid = UUID.randomUUID(); 49 | String friendlyId = FriendlyIds.toFriendlyId(uuid); 50 | String rawUuid = uuid.toString(); 51 | 52 | // when/then - using raw UUID as path variable 53 | mockMvc.perform(get("/items/{id}", rawUuid) 54 | .accept(MediaType.APPLICATION_JSON)) 55 | .andExpect(status().isOk()) 56 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 57 | .andExpect(jsonPath("$.id", is(friendlyId))) 58 | .andExpect(jsonPath("$.rawId", is(rawUuid))) 59 | .andExpect(jsonPath("$.friendlyUuid", is(friendlyId))) 60 | .andExpect(jsonPath("$.friendlyId", is(friendlyId))); 61 | } 62 | 63 | @Test 64 | void shouldDeserializeAndSerialize() throws Exception { 65 | // given 66 | UUID uuid = UUID.randomUUID(); 67 | String friendlyId = FriendlyIds.toFriendlyId(uuid); 68 | String json = """ 69 | {"id": "%s"} 70 | """.formatted(friendlyId); 71 | 72 | // when/then 73 | mockMvc.perform(post("/items") 74 | .contentType(MediaType.APPLICATION_JSON) 75 | .content(json) 76 | .accept(MediaType.APPLICATION_JSON)) 77 | .andExpect(status().isOk()) 78 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 79 | .andExpect(jsonPath("$.id", is(friendlyId))); 80 | } 81 | 82 | @Test 83 | void shouldGenerateAllIdsWhenNotProvided() throws Exception { 84 | // given 85 | String json = "{}"; 86 | 87 | // when/then 88 | mockMvc.perform(post("/items") 89 | .contentType(MediaType.APPLICATION_JSON) 90 | .content(json) 91 | .accept(MediaType.APPLICATION_JSON)) 92 | .andExpect(status().isOk()) 93 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 94 | .andExpect(jsonPath("$.id", notNullValue())) 95 | .andExpect(jsonPath("$.rawId", notNullValue())) 96 | .andExpect(jsonPath("$.friendlyUuid", notNullValue())) 97 | .andExpect(jsonPath("$.friendlyId", notNullValue())); 98 | } 99 | 100 | @Test 101 | void shouldAcceptFriendlyIdAsRequestParam() throws Exception { 102 | // given 103 | UUID uuid = UUID.randomUUID(); 104 | String friendlyId = FriendlyIds.toFriendlyId(uuid); 105 | String rawUuid = uuid.toString(); 106 | 107 | // when/then - using FriendlyId as ?id=xxx 108 | mockMvc.perform(get("/items") 109 | .param("id", friendlyId) 110 | .accept(MediaType.APPLICATION_JSON)) 111 | .andExpect(status().isOk()) 112 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 113 | .andExpect(jsonPath("$.id", is(friendlyId))) 114 | .andExpect(jsonPath("$.rawId", is(rawUuid))); 115 | } 116 | 117 | @Test 118 | void shouldAcceptUuidAsRequestParam() throws Exception { 119 | // given 120 | UUID uuid = UUID.randomUUID(); 121 | String friendlyId = FriendlyIds.toFriendlyId(uuid); 122 | String rawUuid = uuid.toString(); 123 | 124 | // when/then - using raw UUID as ?id=xxx 125 | mockMvc.perform(get("/items") 126 | .param("id", rawUuid) 127 | .accept(MediaType.APPLICATION_JSON)) 128 | .andExpect(status().isOk()) 129 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 130 | .andExpect(jsonPath("$.id", is(friendlyId))) 131 | .andExpect(jsonPath("$.rawId", is(rawUuid))); 132 | } 133 | 134 | @Test 135 | void shouldAcceptFriendlyIdTypeAsRequestParam() throws Exception { 136 | // given 137 | UUID uuid = UUID.randomUUID(); 138 | String friendlyId = FriendlyIds.toFriendlyId(uuid); 139 | String rawUuid = uuid.toString(); 140 | 141 | // when/then - using FriendlyId string with @RequestParam FriendlyId type 142 | mockMvc.perform(get("/items/by-friendly-id") 143 | .param("id", friendlyId) 144 | .accept(MediaType.APPLICATION_JSON)) 145 | .andExpect(status().isOk()) 146 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 147 | .andExpect(jsonPath("$.id", is(friendlyId))) 148 | .andExpect(jsonPath("$.rawId", is(rawUuid))); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /friendly-id-jpa/README.md: -------------------------------------------------------------------------------- 1 | # FriendlyId JPA Integration 2 | 3 | JPA AttributeConverter for transparent UUID to FriendlyId conversion in entity mappings. 4 | 5 | ## Overview 6 | 7 | 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. 8 | 9 | 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). 10 | 11 | ## Maven Dependency 12 | 13 | ```xml 14 | 15 | com.devskiller.friendly-id 16 | friendly-id-jpa 17 | 1.1.1-SNAPSHOT 18 | 19 | ``` 20 | 21 | ## Automatic Usage (Recommended) 22 | 23 | The converter is **automatically applied** to all FriendlyId attributes thanks to `@Converter(autoApply = true)`. No configuration needed! 24 | 25 | ```java 26 | import com.devskiller.friendly_id.type.FriendlyId; 27 | import jakarta.persistence.*; 28 | 29 | @Entity 30 | @Table(name = "users") 31 | public class User { 32 | 33 | @Id 34 | private FriendlyId id; // Automatically converted to/from UUID 35 | 36 | private String name; 37 | 38 | // getters/setters 39 | } 40 | ``` 41 | 42 | ## Usage Examples 43 | 44 | ### Creating Entities 45 | 46 | ```java 47 | // Create with random FriendlyId 48 | User user = new User(); 49 | user.setId(FriendlyId.random()); 50 | user.setName("John Doe"); 51 | em.persist(user); 52 | 53 | // Create from FriendlyId string 54 | User user = new User(); 55 | user.setId(FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb")); 56 | user.setName("Jane Doe"); 57 | em.persist(user); 58 | ``` 59 | 60 | ### Querying 61 | 62 | ```java 63 | // JPQL query 64 | FriendlyId userId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); 65 | User user = em.createQuery("SELECT u FROM User u WHERE u.id = :id", User.class) 66 | .setParameter("id", userId) 67 | .getSingleResult(); 68 | 69 | // Find by ID 70 | User user = em.find(User.class, FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb")); 71 | 72 | // Criteria API 73 | CriteriaBuilder cb = em.getCriteriaBuilder(); 74 | CriteriaQuery query = cb.createQuery(User.class); 75 | Root root = query.from(User.class); 76 | query.where(cb.equal(root.get("id"), userId)); 77 | List users = em.createQuery(query).getResultList(); 78 | ``` 79 | 80 | ### Native Queries 81 | 82 | For native queries, use the UUID directly: 83 | 84 | ```java 85 | FriendlyId userId = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); 86 | User user = em.createNativeQuery( 87 | "SELECT * FROM users WHERE id = ?", 88 | User.class) 89 | .setParameter(1, userId.uuid()) // Use .uuid() for native queries 90 | .getSingleResult(); 91 | ``` 92 | 93 | ### Pretty Printing 94 | 95 | ```java 96 | User user = em.find(User.class, someId); 97 | System.out.println("User ID: " + user.getId()); 98 | // Prints: User ID: 5wbwf6yUxVBcr48AMbz9cb 99 | ``` 100 | 101 | ## Manual Application (Optional) 102 | 103 | If you disabled autoApply or need explicit control: 104 | 105 | ```java 106 | @Entity 107 | public class User { 108 | @Id 109 | @Convert(converter = FriendlyIdConverter.class) 110 | private FriendlyId id; 111 | 112 | private String name; 113 | } 114 | ``` 115 | 116 | ## Spring Data JPA Example 117 | 118 | ```java 119 | import org.springframework.data.jpa.repository.JpaRepository; 120 | 121 | public interface UserRepository extends JpaRepository { 122 | 123 | // Derived query methods work automatically 124 | Optional findByName(String name); 125 | 126 | // Custom queries with FriendlyId parameters 127 | @Query("SELECT u FROM User u WHERE u.id = :id") 128 | Optional findByFriendlyId(@Param("id") FriendlyId id); 129 | } 130 | 131 | // Usage 132 | FriendlyId id = FriendlyId.fromString("5wbwf6yUxVBcr48AMbz9cb"); 133 | Optional user = userRepository.findById(id); 134 | ``` 135 | 136 | ## How It Works 137 | 138 | The `FriendlyIdConverter` implements `jakarta.persistence.AttributeConverter`: 139 | 140 | - **Database Column Type**: `UUID` (16 bytes) 141 | - **Entity Attribute Type**: `FriendlyId` value object (~28 bytes in memory) 142 | - **Conversion**: Bidirectional and automatic 143 | 144 | ## Memory Efficiency 145 | 146 | | Type | Memory Usage | Notes | 147 | |------|-------------|-------| 148 | | UUID | 16 bytes | Database storage | 149 | | FriendlyId | ~28 bytes | 16 bytes UUID + ~12 bytes object header | 150 | | String | ~40-50 bytes | FriendlyId as String (avoided) | 151 | 152 | **Result**: ~30-40% memory savings compared to storing FriendlyId as String 153 | 154 | ## Benefits 155 | 156 | - **Zero Configuration**: Works automatically with `autoApply = true` 157 | - **Memory Efficient**: Store UUIDs internally, compute strings only when needed 158 | - **Type Safety**: Strong typing prevents mixing UUIDs with FriendlyIds 159 | - **Database Efficiency**: Store compact UUIDs in the database 160 | - **Transparent**: No manual conversion in entity code 161 | - **Pretty Printing**: Automatic FriendlyId string representation via `toString()` 162 | - **Spring Data Compatible**: Works seamlessly with Spring Data JPA repositories 163 | 164 | ## Database Schema 165 | 166 | The database column should be UUID type: 167 | 168 | ```sql 169 | CREATE TABLE users ( 170 | id UUID PRIMARY KEY, 171 | name VARCHAR(255) 172 | ); 173 | ``` 174 | 175 | ## See Also 176 | 177 | - [JPA AttributeConverter Documentation](https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/attributeconverter) 178 | - [FriendlyId Core Library](../friendly-id/) 179 | - [FriendlyId Value Object](../friendly-id/src/main/java/com/devskiller/friendly_id/type/FriendlyId.java) 180 | - [FriendlyId jOOQ Integration](../friendly-id-jooq/) 181 | -------------------------------------------------------------------------------- /friendly-id/src/test/java/com/devskiller/friendly_id/type/FriendlyIdTest.java: -------------------------------------------------------------------------------- 1 | package com.devskiller.friendly_id.type; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.UUID; 6 | 7 | import static com.devskiller.friendly_id.FriendlyIds.toFriendlyId; 8 | import static com.devskiller.friendly_id.type.FriendlyId.friendlyId; 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | class FriendlyIdTest { 12 | 13 | @Test 14 | void shouldCreateFromUuid() { 15 | // given 16 | UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); 17 | 18 | // when 19 | FriendlyId friendlyId = FriendlyId.of(uuid); 20 | 21 | // then 22 | assertEquals(uuid, friendlyId.uuid()); 23 | } 24 | 25 | @Test 26 | void shouldCreateFromString() { 27 | // given 28 | String friendlyIdString = "5wbwf6yUxVBcr48AMbz9cb"; 29 | 30 | // when 31 | FriendlyId friendlyId = FriendlyId.parse(friendlyIdString); 32 | 33 | // then 34 | assertEquals(friendlyIdString, friendlyId.toString()); 35 | } 36 | 37 | @Test 38 | void shouldCreateRandom() { 39 | // when 40 | FriendlyId friendlyId1 = FriendlyId.random(); 41 | FriendlyId friendlyId2 = FriendlyId.random(); 42 | 43 | // then 44 | assertNotEquals(friendlyId1, friendlyId2); 45 | assertNotNull(friendlyId1.uuid()); 46 | assertNotNull(friendlyId2.uuid()); 47 | } 48 | 49 | @Test 50 | void shouldConvertToString() { 51 | // given 52 | UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); 53 | FriendlyId friendlyId = FriendlyId.of(uuid); 54 | 55 | // when 56 | String result = friendlyId.toString(); 57 | 58 | // then 59 | assertEquals(toFriendlyId(uuid), result); 60 | } 61 | 62 | @Test 63 | void shouldBeEqualWhenUuidIsEqual() { 64 | // given 65 | UUID uuid = UUID.randomUUID(); 66 | FriendlyId id1 = FriendlyId.of(uuid); 67 | FriendlyId id2 = FriendlyId.of(uuid); 68 | 69 | // then 70 | assertEquals(id1, id2); 71 | assertEquals(id1.hashCode(), id2.hashCode()); 72 | } 73 | 74 | @Test 75 | void shouldNotBeEqualWhenUuidIsDifferent() { 76 | // given 77 | FriendlyId id1 = FriendlyId.random(); 78 | FriendlyId id2 = FriendlyId.random(); 79 | 80 | // then 81 | assertNotEquals(id1, id2); 82 | } 83 | 84 | @Test 85 | void shouldBeComparable() { 86 | // given 87 | UUID uuid1 = UUID.fromString("00000000-0000-0000-0000-000000000001"); 88 | UUID uuid2 = UUID.fromString("00000000-0000-0000-0000-000000000002"); 89 | FriendlyId id1 = FriendlyId.of(uuid1); 90 | FriendlyId id1Copy = FriendlyId.of(uuid1); 91 | FriendlyId id2 = FriendlyId.of(uuid2); 92 | 93 | // then 94 | assertTrue(id1.compareTo(id2) < 0); 95 | assertTrue(id2.compareTo(id1) > 0); 96 | assertEquals(0, id1.compareTo(id1Copy)); 97 | } 98 | 99 | @Test 100 | void shouldThrowExceptionWhenUuidIsNull() { 101 | // when/then 102 | assertThrows(NullPointerException.class, () -> FriendlyId.of(null)); 103 | } 104 | 105 | @Test 106 | void shouldThrowExceptionWhenStringIsNull() { 107 | // when/then 108 | assertThrows(NullPointerException.class, () -> FriendlyId.parse(null)); 109 | } 110 | 111 | @Test 112 | void shouldBeReversible() { 113 | // given 114 | UUID originalUuid = UUID.randomUUID(); 115 | FriendlyId friendlyId = FriendlyId.of(originalUuid); 116 | 117 | // when 118 | String friendlyIdString = friendlyId.toString(); 119 | FriendlyId reconstructed = FriendlyId.parse(friendlyIdString); 120 | 121 | // then 122 | assertEquals(friendlyId, reconstructed); 123 | assertEquals(originalUuid, reconstructed.uuid()); 124 | } 125 | 126 | @Test 127 | void shouldBeEqualRegardlessOfCreationMethod() { 128 | // given 129 | UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); 130 | 131 | // when 132 | FriendlyId fromUuid = FriendlyId.of(uuid); 133 | FriendlyId parsed = FriendlyId.parse(toFriendlyId(uuid)); 134 | 135 | // then 136 | assertEquals(fromUuid, parsed); 137 | assertEquals(fromUuid.hashCode(), parsed.hashCode()); 138 | assertEquals(fromUuid.toString(), parsed.toString()); 139 | } 140 | 141 | @Test 142 | void shouldParseFriendlyIdString() { 143 | // given 144 | String friendlyIdString = "5wbwf6yUxVBcr48AMbz9cb"; 145 | 146 | // when 147 | FriendlyId friendlyId = FriendlyId.parse(friendlyIdString); 148 | 149 | // then 150 | assertEquals(friendlyIdString, friendlyId.value()); 151 | } 152 | 153 | @Test 154 | void shouldParseUuidString() { 155 | // given 156 | UUID uuid = UUID.fromString("c3587ec5-0976-497f-8374-61e0c2ea3da5"); 157 | String uuidString = uuid.toString(); 158 | 159 | // when 160 | FriendlyId friendlyId = FriendlyId.parse(uuidString); 161 | 162 | // then 163 | assertEquals(uuid, friendlyId.toUuid()); 164 | assertEquals("5wbwf6yUxVBcr48AMbz9cb", friendlyId.value()); 165 | } 166 | 167 | @Test 168 | void shouldParseBothFormatsToSameResult() { 169 | // given 170 | String friendlyIdString = "5wbwf6yUxVBcr48AMbz9cb"; 171 | String uuidString = "c3587ec5-0976-497f-8374-61e0c2ea3da5"; 172 | 173 | // when 174 | FriendlyId fromFriendlyId = FriendlyId.parse(friendlyIdString); 175 | FriendlyId fromUuid = FriendlyId.parse(uuidString); 176 | 177 | // then 178 | assertEquals(fromFriendlyId, fromUuid); 179 | } 180 | 181 | @Test 182 | void shouldCreateWithStaticImportFriendlyMethod() { 183 | // given 184 | UUID uuid = UUID.randomUUID(); 185 | 186 | // when 187 | FriendlyId id = friendlyId(uuid); 188 | 189 | // then 190 | assertEquals(uuid, id.toUuid()); 191 | } 192 | 193 | @Test 194 | void shouldReturnUuidWithToUuid() { 195 | // given 196 | UUID uuid = UUID.randomUUID(); 197 | FriendlyId id = FriendlyId.of(uuid); 198 | 199 | // then 200 | assertEquals(uuid, id.toUuid()); 201 | assertEquals(id.uuid(), id.toUuid()); 202 | } 203 | 204 | @Test 205 | void shouldReturnValueAsString() { 206 | // given 207 | UUID uuid = UUID.fromString("7b0f3a3e-3b3a-4b3a-8b3a-3b3a3b3a3b3a"); 208 | FriendlyId id = FriendlyId.of(uuid); 209 | 210 | // when 211 | String value = id.value(); 212 | 213 | // then 214 | assertEquals(id.toString(), value); 215 | assertEquals(toFriendlyId(uuid), value); 216 | } 217 | 218 | } 219 | --------------------------------------------------------------------------------