diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3fe3762..4829901 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
+- Configurable UUID⇄FriendlyId encoding (`FriendlyIdEncoding`): `STANDARD` (default, the bit-shifting
+ pairing used since 1.1.0) and `LEGACY` (Szudzik's elegant pairing from the 1.0.x line). Set globally
+ with `FriendlyIds.setEncoding(...)` or, with the Spring Boot starter, via the
+ `com.devskiller.friendly-id.encoding=legacy` property. The two encodings are wire-incompatible —
+ decoding an identifier with the wrong one silently yields a different UUID — so services must keep
+ the encoding their identifiers were issued with (pinned by test vectors generated from released
+ 1.0.4 and 1.1.0 artifacts).
- FriendlyId value object type (`com.devskiller.friendly_id.type.FriendlyId`) as an alternative to raw UUID
- JPA integration module (`friendly-id-jpa`) with automatic AttributeConverter
- OpenFeign integration module (`friendly-id-openfeign`) for FriendlyId support in Feign clients
diff --git a/README.md b/README.md
index 5a00822..d088b8b 100644
--- a/README.md
+++ b/README.md
@@ -306,6 +306,32 @@ UUID and `FriendlyId` parameters are automatically converted to FriendlyId strin
Version 2.0 introduces several breaking changes to support Spring Boot 4 and Jackson 3.
+#### Encoding compatibility (1.0.x vs 1.1.0+)
+
+Version 1.1.0 changed the internal UUID pairing algorithm, so **1.0.x and 1.1.0+ produce
+different FriendlyId strings for the same UUID** — and decoding an identifier with the wrong
+algorithm silently yields a different UUID. Since 2.0 the algorithm is selectable:
+
+| Encoding | Wire-compatible with | Notes |
+|------------|----------------------|-------|
+| `STANDARD` | 1.1.0 and newer | default |
+| `LEGACY` | 1.0.x | Szudzik's elegant pairing |
+
+Services upgrading **from 1.0.x** must opt into the legacy encoding to keep their published
+identifiers stable — either programmatically at startup:
+
+```java
+FriendlyIds.setEncoding(FriendlyIdEncoding.LEGACY);
+```
+
+or, with the Spring Boot starter, via a property:
+
+```properties
+com.devskiller.friendly-id.encoding=legacy
+```
+
+Services upgrading from 1.1.0+ need no changes — `STANDARD` is the default.
+
#### Requirements
| Version | Java | Spring Boot | Jackson |
diff --git a/friendly-id-spring-boot-starter/pom.xml b/friendly-id-spring-boot-starter/pom.xml
index 18f3d2b..6dd1768 100644
--- a/friendly-id-spring-boot-starter/pom.xml
+++ b/friendly-id-spring-boot-starter/pom.xml
@@ -35,5 +35,15 @@
* Automatically enables FriendlyId converters and Jackson module when Spring Boot is detected. * Can be disabled by setting {@code com.devskiller.friendly-id.enabled=false} in application properties. + *
+ * The encoding can be selected with {@code com.devskiller.friendly-id.encoding} — set it to
+ * {@code legacy} to stay wire-compatible with identifiers issued by the friendly-id 1.0.x line.
*/
@AutoConfiguration
@ConditionalOnWebApplication
@@ -20,7 +25,12 @@
havingValue = "true",
matchIfMissing = true
)
+@EnableConfigurationProperties(FriendlyIdProperties.class)
@EnableFriendlyId
public class FriendlyIdAutoConfiguration {
+ FriendlyIdAutoConfiguration(FriendlyIdProperties properties) {
+ FriendlyIds.setEncoding(properties.getEncoding());
+ }
+
}
diff --git a/friendly-id-spring-boot-starter/src/main/java/com/devskiller/friendly_id/boot/FriendlyIdProperties.java b/friendly-id-spring-boot-starter/src/main/java/com/devskiller/friendly_id/boot/FriendlyIdProperties.java
new file mode 100644
index 0000000..26c52d4
--- /dev/null
+++ b/friendly-id-spring-boot-starter/src/main/java/com/devskiller/friendly_id/boot/FriendlyIdProperties.java
@@ -0,0 +1,40 @@
+package com.devskiller.friendly_id.boot;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import com.devskiller.friendly_id.FriendlyIdEncoding;
+
+/**
+ * Configuration properties for the FriendlyId Spring Boot integration.
+ */
+@ConfigurationProperties(prefix = "com.devskiller.friendly-id")
+public class FriendlyIdProperties {
+
+ /**
+ * Whether to enable the FriendlyId auto-configuration.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Encoding used for UUID to FriendlyId conversion. Use LEGACY to stay
+ * wire-compatible with identifiers issued by the friendly-id 1.0.x line.
+ */
+ private FriendlyIdEncoding encoding = FriendlyIdEncoding.STANDARD;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public FriendlyIdEncoding getEncoding() {
+ return encoding;
+ }
+
+ public void setEncoding(FriendlyIdEncoding encoding) {
+ this.encoding = encoding;
+ }
+
+}
diff --git a/friendly-id-spring-boot-starter/src/test/java/com/devskiller/friendly_id/boot/FriendlyIdAutoConfigurationTest.java b/friendly-id-spring-boot-starter/src/test/java/com/devskiller/friendly_id/boot/FriendlyIdAutoConfigurationTest.java
new file mode 100644
index 0000000..76eff8e
--- /dev/null
+++ b/friendly-id-spring-boot-starter/src/test/java/com/devskiller/friendly_id/boot/FriendlyIdAutoConfigurationTest.java
@@ -0,0 +1,49 @@
+package com.devskiller.friendly_id.boot;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
+
+import com.devskiller.friendly_id.FriendlyIdEncoding;
+import com.devskiller.friendly_id.FriendlyIds;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class FriendlyIdAutoConfigurationTest {
+
+ private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(FriendlyIdAutoConfiguration.class));
+
+ @AfterEach
+ void restoreDefaultEncoding() {
+ FriendlyIds.setEncoding(FriendlyIdEncoding.STANDARD);
+ }
+
+ @Test
+ void usesStandardEncodingByDefault() {
+ contextRunner.run(context -> {
+ assertThat(context).hasSingleBean(FriendlyIdAutoConfiguration.class);
+ assertThat(FriendlyIds.getEncoding()).isEqualTo(FriendlyIdEncoding.STANDARD);
+ });
+ }
+
+ @Test
+ void encodingPropertySwitchesToLegacy() {
+ contextRunner
+ .withPropertyValues("com.devskiller.friendly-id.encoding=legacy")
+ .run(context -> {
+ assertThat(context).hasSingleBean(FriendlyIdAutoConfiguration.class);
+ assertThat(FriendlyIds.getEncoding()).isEqualTo(FriendlyIdEncoding.LEGACY);
+ });
+ }
+
+ @Test
+ void canBeDisabled() {
+ contextRunner
+ .withPropertyValues("com.devskiller.friendly-id.enabled=false")
+ .run(context -> assertThat(context).doesNotHaveBean(FriendlyIdAutoConfiguration.class));
+ }
+
+}
diff --git a/friendly-id/pom.xml b/friendly-id/pom.xml
index f3497c3..b0cbdbb 100644
--- a/friendly-id/pom.xml
+++ b/friendly-id/pom.xml
@@ -36,6 +36,9 @@
+ * A double estimate is accurate to ~53 bits, one Newton step fixes the rest of a root of up to 64 bits
+ * (paired UUIDs are below 2^128), and the loops correct the final off-by-one.
+ *
+ * TODO: replace with {@link BigInteger#sqrt()} after moving to JDK 25 — it is ~9x slower than this on
+ * JDK 21 but faster on JDK 25.
+ */
+ static BigInteger sqrt(BigInteger n) {
+ if (n.signum() == 0) {
+ return n;
+ }
+ double root = Math.sqrt(n.doubleValue());
+ // bits below the 53 significant ones are zero, so scaling them off keeps the conversion exact
+ int shift = Math.max(0, Math.getExponent(root) - 52);
+ BigInteger a = BigInteger.valueOf((long) Math.scalb(root, -shift)).shiftLeft(shift);
+ a = a.add(n.divide(a)).shiftRight(1);
+ while (a.multiply(a).compareTo(n) > 0) {
+ a = a.subtract(ONE);
+ }
+ for (BigInteger next = a.add(ONE); next.multiply(next).compareTo(n) <= 0; next = a.add(ONE)) {
+ a = next;
+ }
+ return a;
+ }
+
+}
diff --git a/friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIdEncoding.java b/friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIdEncoding.java
new file mode 100644
index 0000000..16189c8
--- /dev/null
+++ b/friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIdEncoding.java
@@ -0,0 +1,60 @@
+package com.devskiller.friendly_id;
+
+import java.math.BigInteger;
+
+/**
+ * Strategy used to map the two 64-bit halves of a UUID onto the single
+ * {@link BigInteger} that is Base62-encoded into a FriendlyId string.
+ *
+ * The two strategies produce incompatible FriendlyId strings for
+ * the same UUID. Decoding a FriendlyId with the wrong strategy does not fail —
+ * it silently yields a different UUID — so a service must keep using the strategy
+ * its identifiers were originally issued with.
+ *
+ *
+ * Intended to be called once during application startup, before any conversion happens.
+ * Identifiers encoded with one strategy silently decode to a different UUID
+ * under the other, so switching at runtime on live traffic is not supported.
+ *
+ * With the Spring Boot starter this can be set declaratively via the
+ * {@code com.devskiller.friendly-id.encoding} property.
+ *
+ * @param friendlyIdEncoding encoding to use, must not be null
+ * @throws NullPointerException if friendlyIdEncoding is null
+ * @since 2.0.0-beta6
+ */
+ public static void setEncoding(FriendlyIdEncoding friendlyIdEncoding) {
+ Objects.requireNonNull(friendlyIdEncoding, "Encoding cannot be null");
+ encoding = friendlyIdEncoding;
+ }
+
+ /**
+ * Returns the global {@link FriendlyIdEncoding}, {@link FriendlyIdEncoding#STANDARD} by default.
+ *
+ * @return the encoding used by all conversions in this library
+ * @since 2.0.0-beta6
+ */
+ public static FriendlyIdEncoding getEncoding() {
+ return encoding;
+ }
+
/**
* Creates a random FriendlyId string.
*
diff --git a/friendly-id/src/main/java/com/devskiller/friendly_id/Url62.java b/friendly-id/src/main/java/com/devskiller/friendly_id/Url62.java
index b7e27f0..df2dcb8 100644
--- a/friendly-id/src/main/java/com/devskiller/friendly_id/Url62.java
+++ b/friendly-id/src/main/java/com/devskiller/friendly_id/Url62.java
@@ -15,7 +15,7 @@ class Url62 {
* @return url62 encoded UUID
*/
static String encode(UUID uuid) {
- BigInteger pair = UuidConverter.toBigInteger(uuid);
+ BigInteger pair = UuidConverter.toBigInteger(uuid, FriendlyIds.getEncoding());
return Base62.encode(pair);
}
@@ -27,7 +27,7 @@ static String encode(UUID uuid) {
*/
static UUID decode(String id) {
BigInteger decoded = Base62.decode(id);
- return UuidConverter.toUuid(decoded);
+ return UuidConverter.toUuid(decoded, FriendlyIds.getEncoding());
}
}
diff --git a/friendly-id/src/main/java/com/devskiller/friendly_id/UuidConverter.java b/friendly-id/src/main/java/com/devskiller/friendly_id/UuidConverter.java
index 5417c6c..4dd4b6a 100644
--- a/friendly-id/src/main/java/com/devskiller/friendly_id/UuidConverter.java
+++ b/friendly-id/src/main/java/com/devskiller/friendly_id/UuidConverter.java
@@ -5,15 +5,15 @@
class UuidConverter {
- static BigInteger toBigInteger(UUID uuid) {
- return BigIntegerPairing.pair(
+ static BigInteger toBigInteger(UUID uuid, FriendlyIdEncoding encoding) {
+ return encoding.pair(
BigInteger.valueOf(uuid.getMostSignificantBits()),
BigInteger.valueOf(uuid.getLeastSignificantBits())
);
}
- static UUID toUuid(BigInteger value) {
- BigInteger[] unpaired = BigIntegerPairing.unpair(value);
+ static UUID toUuid(BigInteger value, FriendlyIdEncoding encoding) {
+ BigInteger[] unpaired = encoding.unpair(value);
return new UUID(unpaired[0].longValueExact(), unpaired[1].longValueExact());
}
diff --git a/friendly-id/src/test/java/com/devskiller/friendly_id/AnalyzeGeneratedIdsTest.java b/friendly-id/src/test/java/com/devskiller/friendly_id/AnalyzeGeneratedIdsTest.java
index 0eac895..64a8fdf 100644
--- a/friendly-id/src/test/java/com/devskiller/friendly_id/AnalyzeGeneratedIdsTest.java
+++ b/friendly-id/src/test/java/com/devskiller/friendly_id/AnalyzeGeneratedIdsTest.java
@@ -17,7 +17,7 @@ class AnalyzeGeneratedIdsTest {
@Test
void analyzeGeneratedValueStatistics() {
for (int i = 0; i < 100_000; i++) {
- this.ids.add(Base62.encode(UuidConverter.toBigInteger(UUID.randomUUID())));
+ this.ids.add(Base62.encode(UuidConverter.toBigInteger(UUID.randomUUID(), FriendlyIds.getEncoding())));
}
IntSummaryStatistics stats = ids.stream().map(String::length).mapToInt(Integer::intValue).summaryStatistics();
diff --git a/friendly-id/src/test/java/com/devskiller/friendly_id/ElegantPairingTest.java b/friendly-id/src/test/java/com/devskiller/friendly_id/ElegantPairingTest.java
new file mode 100644
index 0000000..67d6cb4
--- /dev/null
+++ b/friendly-id/src/test/java/com/devskiller/friendly_id/ElegantPairingTest.java
@@ -0,0 +1,60 @@
+package com.devskiller.friendly_id;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.math.BigInteger;
+import java.util.Random;
+import java.util.stream.IntStream;
+
+import static com.devskiller.friendly_id.ElegantPairing.pair;
+import static com.devskiller.friendly_id.ElegantPairing.sqrt;
+import static com.devskiller.friendly_id.ElegantPairing.unpair;
+import static java.math.BigInteger.ONE;
+import static java.math.BigInteger.valueOf;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ElegantPairingTest {
+
+ private static final long[] EXTREMES = {0, 1, -1, Long.MAX_VALUE, Long.MIN_VALUE, Long.MAX_VALUE - 1, Long.MIN_VALUE + 1};
+
+ @Test
+ void sqrtShouldMatchFloorSqrtForSmallValues() {
+ for (long n = 0; n < 10_000; n++) {
+ assertThat(sqrt(valueOf(n))).as("sqrt(%d)", n).isEqualTo(valueOf(n).sqrt());
+ }
+ }
+
+ // pairing two longs yields values below 2^128 (roots up to 64 bits); go a bit beyond that range
+ @ParameterizedTest
+ @MethodSource("rootBitLengths")
+ void sqrtShouldMatchFloorSqrtAroundPerfectSquares(int rootBitLength) {
+ Random random = new Random(rootBitLength);
+ for (int i = 0; i < 1000; i++) {
+ BigInteger root = new BigInteger(rootBitLength, random).setBit(rootBitLength - 1);
+ BigInteger square = root.multiply(root);
+
+ assertThat(sqrt(square.subtract(ONE))).isEqualTo(square.subtract(ONE).sqrt());
+ assertThat(sqrt(square)).isEqualTo(root);
+ assertThat(sqrt(square.add(ONE))).isEqualTo(root);
+ }
+ }
+
+ @Test
+ void pairingExtremeLongsShouldBeReversible() {
+ for (long x : EXTREMES) {
+ for (long y : EXTREMES) {
+ BigInteger paired = pair(valueOf(x), valueOf(y));
+
+ assertThat(sqrt(paired)).isEqualTo(paired.sqrt());
+ assertThat(unpair(paired)).containsExactly(valueOf(x), valueOf(y));
+ }
+ }
+ }
+
+ static IntStream rootBitLengths() {
+ return IntStream.rangeClosed(1, 66);
+ }
+
+}
diff --git a/friendly-id/src/test/java/com/devskiller/friendly_id/FriendlyIdEncodingTest.java b/friendly-id/src/test/java/com/devskiller/friendly_id/FriendlyIdEncodingTest.java
new file mode 100644
index 0000000..5fc9502
--- /dev/null
+++ b/friendly-id/src/test/java/com/devskiller/friendly_id/FriendlyIdEncodingTest.java
@@ -0,0 +1,83 @@
+package com.devskiller.friendly_id;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Pins the wire format of both encodings against vectors generated with released
+ * artifacts: LEGACY vectors come from friendly-id 1.0.4 (ElegantPairing era),
+ * STANDARD vectors from 1.1.0/2.0.0-beta5 (BigIntegerPairing). These must never
+ * change — consumers persist FriendlyIds externally.
+ */
+class FriendlyIdEncodingTest {
+
+ private static final UUID UUID_1 = UUID.fromString("d493e6d3-6a6a-4cf2-8990-46366c75064f");
+ private static final UUID UUID_2 = UUID.fromString("1024138f-684f-4311-b539-774fd3ef3a9c");
+ private static final UUID UUID_ZERO = UUID.fromString("00000000-0000-0000-0000-000000000000");
+ private static final UUID UUID_ALL_BITS = UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff");
+ private static final UUID UUID_LSB_SIGN_BIT = UUID.fromString("00000000-0000-0000-8000-000000000000");
+
+ @AfterEach
+ void restoreDefaultEncoding() {
+ FriendlyIds.setEncoding(FriendlyIdEncoding.STANDARD);
+ }
+
+ @Test
+ void standardEncodingIsTheDefault() {
+ assertThat(FriendlyIds.getEncoding()).isEqualTo(FriendlyIdEncoding.STANDARD);
+ }
+
+ @Test
+ void standardEncodingMatchesVectorsFrom110AndNewer() {
+ assertThat(FriendlyIds.toFriendlyId(UUID_1)).isEqualTo("6T7xno6b7EKyT6xrj2iJo7");
+ assertThat(FriendlyIds.toFriendlyId(UUID_2)).isEqualTo("USMZy4J8qe2K9yZBxR4Hs");
+ assertThat(FriendlyIds.toFriendlyId(UUID_ZERO)).isEqualTo("0");
+ assertThat(FriendlyIds.toFriendlyId(UUID_ALL_BITS)).isEqualTo("7n42DGM5Tflk9n8mt7Fhc7");
+ assertThat(FriendlyIds.toFriendlyId(UUID_LSB_SIGN_BIT)).isEqualTo("AzL8n0Y58m8");
+ }
+
+ @Test
+ void legacyEncodingMatchesVectorsFrom104() {
+ FriendlyIds.setEncoding(FriendlyIdEncoding.LEGACY);
+
+ assertThat(FriendlyIds.toFriendlyId(UUID_1)).isEqualTo("6fZlmOHtnSlxGJGX7SMFg4");
+ assertThat(FriendlyIds.toFriendlyId(UUID_2)).isEqualTo("2er4Hm3VwGW3J0lfwrvQYd");
+ assertThat(FriendlyIds.toFriendlyId(UUID_ZERO)).isEqualTo("0");
+ assertThat(FriendlyIds.toFriendlyId(UUID_ALL_BITS)).isEqualTo("3");
+ assertThat(FriendlyIds.toFriendlyId(UUID_LSB_SIGN_BIT)).isEqualTo("7n42DGM5Tfl2CQZcquv8Vd");
+ }
+
+ @Test
+ void legacyDecodingMatchesVectorsFrom104() {
+ FriendlyIds.setEncoding(FriendlyIdEncoding.LEGACY);
+
+ assertThat(FriendlyIds.toUuid("6fZlmOHtnSlxGJGX7SMFg4")).isEqualTo(UUID_1);
+ assertThat(FriendlyIds.toUuid("2er4Hm3VwGW3J0lfwrvQYd")).isEqualTo(UUID_2);
+ assertThat(FriendlyIds.toUuid("3")).isEqualTo(UUID_ALL_BITS);
+ }
+
+ @Test
+ void legacyEncodingIsReversible() {
+ FriendlyIds.setEncoding(FriendlyIdEncoding.LEGACY);
+
+ for (int i = 0; i < 1000; i++) {
+ UUID uuid = UUID.randomUUID();
+ assertThat(FriendlyIds.toUuid(FriendlyIds.toFriendlyId(uuid))).isEqualTo(uuid);
+ }
+ }
+
+ @Test
+ void encodingSwitchAffectsAllEntryPoints() {
+ FriendlyIds.setEncoding(FriendlyIdEncoding.LEGACY);
+
+ assertThat(com.devskiller.friendly_id.type.FriendlyId.of(UUID_1).value())
+ .isEqualTo("6fZlmOHtnSlxGJGX7SMFg4");
+ assertThat(com.devskiller.friendly_id.type.FriendlyId.parse("6fZlmOHtnSlxGJGX7SMFg4").toUuid())
+ .isEqualTo(UUID_1);
+ }
+
+}
+ *
+ *
+ * @since 2.0.0-beta6
+ * @see FriendlyIds#setEncoding(FriendlyIdEncoding)
+ */
+public enum FriendlyIdEncoding {
+
+ /**
+ * Bit-shifting pairing ({@code hi * 2^64 + unsigned(lo)}), the default encoding
+ * since friendly-id 1.1.0.
+ */
+ STANDARD {
+ @Override
+ BigInteger pair(BigInteger hi, BigInteger lo) {
+ return BigIntegerPairing.pair(hi, lo);
+ }
+
+ @Override
+ BigInteger[] unpair(BigInteger value) {
+ return BigIntegerPairing.unpair(value);
+ }
+ },
+
+ /**
+ * Szudzik's elegant pairing, the encoding used by the friendly-id 1.0.x line.
+ * Use this to stay wire-compatible with identifiers issued by 1.0.x.
+ */
+ LEGACY {
+ @Override
+ BigInteger pair(BigInteger hi, BigInteger lo) {
+ return ElegantPairing.pair(hi, lo);
+ }
+
+ @Override
+ BigInteger[] unpair(BigInteger value) {
+ return ElegantPairing.unpair(value);
+ }
+ };
+
+ abstract BigInteger pair(BigInteger hi, BigInteger lo);
+
+ abstract BigInteger[] unpair(BigInteger value);
+
+}
diff --git a/friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIds.java b/friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIds.java
index 5899747..e7330d0 100644
--- a/friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIds.java
+++ b/friendly-id/src/main/java/com/devskiller/friendly_id/FriendlyIds.java
@@ -27,10 +27,42 @@
*/
public final class FriendlyIds {
+ private static volatile FriendlyIdEncoding encoding = FriendlyIdEncoding.STANDARD;
+
private FriendlyIds() {
// utility class
}
+ /**
+ * Sets the global {@link FriendlyIdEncoding} used by all conversions in this library
+ * (including the Jackson, JPA, jOOQ, OpenFeign and Spring integrations).
+ *