├── .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 |
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 | *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 RepresentationModel4 | * 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 StdSerializer13 | * 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 JpaRepository12 | * 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
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
14 | * Add this annotation to a {@code @Configuration} class to enable automatic conversion
15 | * between FriendlyId strings and UUIDs in:
16 | *
22 | * Example usage:
23 | *
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
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 | *
19 | * Supported conversions:
20 | *
18 | * This decoder wraps the default decoder and intercepts String responses that should be
19 | * converted to UUID or FriendlyId value objects.
20 | *
22 | * Supported conversions:
23 | *
19 | * This configuration:
20 | *
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
15 | * The FriendlyId is automatically converted to/from UUID in the database
16 | * thanks to the FriendlyIdConverter with @Converter(autoApply = true).
17 | *
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 | *
58 | * This method auto-detects the format:
59 | *
15 | * This configuration can be used with {@code @FeignClient} to enable FriendlyId support:
16 | *
29 | * The configuration registers custom encoder and decoder that:
30 | *
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
13 | * This demo shows:
14 | *
22 | * Access points:
23 | *
22 | * This test shows that FriendlyId works seamlessly across the entire stack:
23 | *
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 | *
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 | *
25 | * The converter is automatically applied to all FriendlyId attributes thanks to the
26 | * {@code @Converter(autoApply = true)} annotation. No additional configuration needed.
27 | *
66 | * If autoApply is disabled or you need explicit control:
67 | *
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 | *
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 | *
25 | * Configure this converter in your jOOQ code generation configuration to apply it
26 | * to specific columns or all UUID columns:
27 | *
21 | * Key features demonstrated:
22 | *
29 | * Example URLs:
30 | *
52 | * Response: JSON array with FriendlyId strings instead of UUIDs.
53 | *
63 | * Example: GET /api/products/5wbwf6yUxVBcr48AMbz9cb
64 | *
66 | * The FriendlyId string from URL is automatically converted to FriendlyId value object
67 | * by Spring's StringToFriendlyIdConverter.
68 | *
80 | * Request body should NOT include 'id' - it will be generated automatically.
81 | *
83 | * Example request:
84 | *
109 | * Example: PUT /api/products/5wbwf6yUxVBcr48AMbz9cb
110 | *
132 | * Example: DELETE /api/products/5wbwf6yUxVBcr48AMbz9cb
133 | *
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 | *
18 | * This type is designed to be used with:
19 | *
29 | * Storing FriendlyId as a value object with UUID internally is more memory-efficient
30 | * than storing the String representation:
31 | *
88 | * This method is designed for static imports:
89 | *
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 | *
17 | *
21 | *
24 | * @Configuration
25 | * @EnableFriendlyId
26 | * public class WebConfig {
27 | * }
28 | *
29 | *
22 | *
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 | *
25 | *
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 |
21 | *
24 | * 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 | *
60 | *
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 | * {@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 | *
32 | *
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(ObjectFactorynumber 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
16 | *
21 | *
25 | *
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 | *
25 | *
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 | Automatic Registration (JPA 2.1+)
24 | * 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
63 | *
64 | * Manual Application (Optional)
65 | * {@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 AttributeConverterUsage with jOOQ Code Generator
24 | * {@code
29 | *
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
24 | *
28 | *
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 | *
86 | * {
87 | * "name": "Laptop",
88 | * "description": "High-performance laptop",
89 | * "price": 1299.99,
90 | * "stock": 10
91 | * }
92 | *
93 | */
94 | @PostMapping
95 | public ResponseEntity
21 | *
26 | *
27 | * Memory Efficiency
28 | *
33 | *
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{@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 | *