├── settings.gradle ├── .gitignore ├── CHANGELOG.md ├── .github └── workflows │ └── test.yml ├── LICENSE ├── src ├── test │ └── java │ │ └── org │ │ └── sqids │ │ ├── AlphabetTests.java │ │ ├── BlockListTests.java │ │ ├── EncodeTests.java │ │ └── MinLengthTests.java └── main │ └── java │ └── org │ └── sqids │ └── Sqids.java └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'sqids' 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | bin/ 3 | build/ 4 | .vscode/ 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | **v0.1.0:** 4 | - Initial implementation of [the spec](https://github.com/sqids/sqids-spec) 5 | - Packaging & cleanup -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Testing 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-java@v3 18 | with: 19 | java-version: '8' 20 | distribution: 'temurin' 21 | - uses: gradle/gradle-build-action@v2 22 | - run: gradle wrapper 23 | - run: ./gradlew test 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Sqids maintainers. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test/java/org/sqids/AlphabetTests.java: -------------------------------------------------------------------------------- 1 | package org.sqids; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class AlphabetTests { 10 | @Test 11 | public void simpleAlphabet() { 12 | Sqids sqids = Sqids.builder() 13 | .alphabet("0123456789abcdef") 14 | .build(); 15 | List numbers = Arrays.asList(1L, 2L, 3L); 16 | String id = "489158"; 17 | Assertions.assertEquals(sqids.encode(numbers), id); 18 | Assertions.assertEquals(sqids.decode(id), numbers); 19 | } 20 | 21 | @Test 22 | public void shortAlphabet() { 23 | Sqids sqids = Sqids.builder() 24 | .alphabet("abc") 25 | .build(); 26 | List numbers = Arrays.asList(1L, 2L, 3L); 27 | Assertions.assertEquals(sqids.decode(sqids.encode(numbers)), numbers); 28 | } 29 | 30 | @Test 31 | public void specialCharsAlphabet() { 32 | Sqids sqids = Sqids.builder() 33 | .alphabet(".\\?") 34 | .build(); 35 | List numbers = Arrays.asList(1L, 2L, 3L); 36 | Assertions.assertEquals(sqids.decode(sqids.encode(numbers)), numbers); 37 | } 38 | 39 | @Test 40 | public void multibyteCharacters() { 41 | Assertions.assertThrows(IllegalArgumentException.class, () -> Sqids.builder() 42 | .alphabet("ë1092") 43 | .build()); 44 | } 45 | 46 | @Test 47 | public void repeatingAlphabetCharacters() { 48 | Assertions.assertThrows(IllegalArgumentException.class, () -> Sqids.builder() 49 | .alphabet("aabcdefg") 50 | .build()); 51 | } 52 | 53 | @Test 54 | public void tooShortOfAnAlphabet() { 55 | Assertions.assertThrows(IllegalArgumentException.class, () -> Sqids.builder() 56 | .alphabet("ab") 57 | .build()); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Sqids Java](https://sqids.org/java) 2 | [![javadoc](https://javadoc.io/badge2/org.sqids/sqids/javadoc.svg)](https://javadoc.io/doc/org.sqids/sqids) 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.sqids/sqids/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.sqids/sqids) 4 | 5 | 6 | [Sqids](https://sqids.org/java) (*pronounced "squids"*) is a small library that lets you **generate unique IDs from 7 | numbers**. It's good for link shortening, fast & URL-safe ID generation and decoding back into numbers for quicker 8 | database lookups. 9 | 10 | Features: 11 | 12 | - **Encode multiple numbers** - generate short IDs from one or several non-negative numbers 13 | - **Quick decoding** - easily decode IDs back into numbers 14 | - **Unique IDs** - generate unique IDs by shuffling the alphabet once 15 | - **ID padding** - provide minimum length to make IDs more uniform 16 | - **URL safe** - auto-generated IDs do not contain common profanity 17 | - **Randomized output** - Sequential input provides nonconsecutive IDs 18 | - **Many implementations** - Support for [40+ programming languages](https://sqids.org/) 19 | 20 | ## 🧰 Use-cases 21 | 22 | Good for: 23 | 24 | - Generating IDs for public URLs (eg: link shortening) 25 | - Generating IDs for internal systems (eg: event tracking) 26 | - Decoding for quicker database lookups (eg: by primary keys) 27 | 28 | Not good for: 29 | 30 | - Sensitive data (this is not an encryption library) 31 | - User IDs (can be decoded revealing user count) 32 | 33 | 34 | ## System Requirements 35 | Java 8 or higher is required. 36 | 37 | 38 | ## 🚀 Getting started 39 | 40 | Import dependency. If you are using Apache Maven, add the following dependency to your pom.xml's dependencies: 41 | 42 | ``` 43 | 44 | org.sqids 45 | sqids 46 | 0.1.0 47 | 48 | ``` 49 | 50 | Alternatively, if you use Gradle or are on Android, add the following to your app's `build.gradle` file under dependencies: 51 | 52 | ``` 53 | implementation 'org.sqids:sqids:0.1.0' 54 | ``` 55 | 56 | ## 👩‍💻 Examples 57 | 58 | Import Sqids via: 59 | 60 | ```java 61 | import org.sqids.Sqids; 62 | ``` 63 | 64 | Simple encode & decode: 65 | 66 | ```java 67 | Sqids sqids=Sqids.builder().build(); 68 | String id=sqids.encode(Arrays.asList(1L,2L,3L)); // "86Rf07" 69 | List numbers=sqids.decode(id); // [1, 2, 3] 70 | ``` 71 | 72 | > **Note** 73 | > 🚧 Because of the algorithm's design, **multiple IDs can decode back into the same sequence of numbers**. If it's 74 | > important to your design that IDs are canonical, you have to manually re-encode decoded numbers and check that the 75 | > generated ID matches. 76 | 77 | Enforce a *minimum* length for IDs: 78 | 79 | ```java 80 | Sqids sqids=Sqids.builder() 81 | .minLength(10) 82 | .build(); 83 | String id=sqids.encode(Arrays.asList(1L,2L,3L)); // "86Rf07xd4z" 84 | List numbers=sqids.decode(id); // [1, 2, 3] 85 | ``` 86 | 87 | Randomize IDs by providing a custom alphabet: 88 | 89 | ```java 90 | Sqids sqids=Sqids.builder() 91 | .alphabet("FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE") 92 | .build(); 93 | String id=sqids.encode(Arrays.asList(1L,2L,3L)); // "B4aajs" 94 | List numbers=sqids.decode(id); // [1, 2, 3] 95 | ``` 96 | 97 | Prevent specific words from appearing anywhere in the auto-generated IDs: 98 | 99 | ```java 100 | Sqids sqids=Sqids.builder() 101 | .blockList(new HashSet<>(Arrays.asList("86Rf07"))) 102 | .build(); 103 | String id=sqids.encode(Arrays.asList(1L,2L,3L)); // "se8ojk" 104 | List numbers=sqids.decode(id); // [1, 2, 3] 105 | ``` 106 | 107 | ## 📝 License 108 | 109 | [MIT](LICENSE) 110 | -------------------------------------------------------------------------------- /src/test/java/org/sqids/BlockListTests.java: -------------------------------------------------------------------------------- 1 | package org.sqids; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class BlockListTests { 11 | @Test 12 | public void blockList() { 13 | Sqids sqids = Sqids.builder() 14 | .build(); 15 | List numbers = Arrays.asList(4572721L); 16 | Assertions.assertEquals(sqids.decode("aho1e"), numbers); 17 | Assertions.assertEquals(sqids.encode(numbers), "JExTR"); 18 | } 19 | 20 | @Test 21 | public void emptyBlockList() { 22 | Sqids sqids = Sqids.builder() 23 | .blockList(new HashSet<>()) 24 | .build(); 25 | List numbers = Arrays.asList(4572721L); 26 | Assertions.assertEquals(sqids.decode("aho1e"), numbers); 27 | Assertions.assertEquals(sqids.encode(numbers), "aho1e"); 28 | } 29 | 30 | @Test 31 | public void nonEmptyBlockList() { 32 | Sqids sqids = Sqids.builder() 33 | .blockList(new HashSet<>(Arrays.asList("ArUO"))) 34 | .build(); 35 | List numbers = Arrays.asList(4572721L); 36 | Assertions.assertEquals(sqids.decode("aho1e"), numbers); 37 | Assertions.assertEquals(sqids.encode(numbers), "aho1e"); 38 | 39 | numbers = Arrays.asList(100000L); 40 | Assertions.assertEquals(sqids.decode("ArUO"), numbers); 41 | Assertions.assertEquals(sqids.encode(numbers), "QyG4"); 42 | Assertions.assertEquals(sqids.decode("QyG4"), numbers); 43 | } 44 | 45 | @Test 46 | public void encodeBlockList() { 47 | Sqids sqids = Sqids.builder() 48 | .blockList(new HashSet<>(Arrays.asList( 49 | "JSwXFaosAN", // normal result of 1st encoding, let's block that word on purpose 50 | "OCjV9JK64o", // result of 2nd encoding 51 | "rBHf", // result of 3rd encoding is `4rBHfOiqd3`, let's block a substring 52 | "79SM", // result of 4th encoding is `dyhgw479SM`, let's block the postfix 53 | "7tE6" // result of 4th encoding is `7tE6jdAHLe`, let's block the prefix 54 | ))) 55 | .build(); 56 | List numbers = Arrays.asList(1000000L, 2000000L); 57 | Assertions.assertEquals(sqids.encode(numbers), "1aYeB7bRUt"); 58 | Assertions.assertEquals(sqids.decode("1aYeB7bRUt"), numbers); 59 | } 60 | 61 | @Test 62 | public void decodeBlockList() { 63 | Sqids sqids = Sqids.builder() 64 | .blockList(new HashSet<>(Arrays.asList( 65 | "86Rf07", 66 | "se8ojk", 67 | "ARsz1p", 68 | "Q8AI49", 69 | "5sQRZO" 70 | ))) 71 | .build(); 72 | List numbers = Arrays.asList(1L, 2L, 3L); 73 | Assertions.assertEquals(sqids.decode("86Rf07"), numbers); 74 | Assertions.assertEquals(sqids.decode("se8ojk"), numbers); 75 | Assertions.assertEquals(sqids.decode("ARsz1p"), numbers); 76 | Assertions.assertEquals(sqids.decode("Q8AI49"), numbers); 77 | Assertions.assertEquals(sqids.decode("5sQRZO"), numbers); 78 | } 79 | 80 | @Test 81 | public void shortBlockList() { 82 | Sqids sqids = Sqids.builder() 83 | .blockList(new HashSet<>(Arrays.asList("pnd"))) 84 | .build(); 85 | List numbers = Arrays.asList(1000L); 86 | Assertions.assertEquals(sqids.decode(sqids.encode(numbers)), numbers); 87 | } 88 | 89 | @Test 90 | public void lowercaseBlockList() { 91 | Sqids sqids = Sqids.builder() 92 | .alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZ") 93 | .blockList(new HashSet<>(Arrays.asList("sxnzkl"))) 94 | .build(); 95 | List numbers = Arrays.asList(1L, 2L, 3L); 96 | Assertions.assertEquals(sqids.encode(numbers), "IBSHOZ"); 97 | Assertions.assertEquals(sqids.decode("IBSHOZ"), numbers); 98 | } 99 | 100 | @Test 101 | public void maxBlockList() { 102 | Sqids sqids = Sqids.builder() 103 | .alphabet("abc") 104 | .minLength(3) 105 | .blockList(new HashSet<>(Arrays.asList( 106 | "cab", 107 | "abc", 108 | "bca"))) 109 | .build(); 110 | Assertions.assertThrows(RuntimeException.class, () -> sqids.encode(Arrays.asList(0L))); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/org/sqids/EncodeTests.java: -------------------------------------------------------------------------------- 1 | package org.sqids; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class EncodeTests { 13 | private final Sqids sqids = Sqids.builder().build(); 14 | 15 | @Test 16 | public void simple() { 17 | List numbers = Arrays.asList(1L, 2L, 3L); 18 | String id = "86Rf07"; 19 | Assertions.assertEquals(sqids.encode(numbers), id); 20 | Assertions.assertEquals(sqids.decode(id), numbers); 21 | } 22 | 23 | @Test 24 | public void differentInputs() { 25 | List numbers = Arrays.asList( 26 | 0L, 27 | 0L, 28 | 0L, 29 | 1L, 30 | 2L, 31 | 3L, 32 | 100L, 33 | 1000L, 34 | 100000L, 35 | 1000000L, 36 | Long.MAX_VALUE); 37 | Assertions.assertEquals(sqids.decode(sqids.encode(numbers)), numbers); 38 | } 39 | 40 | @Test 41 | public void incrementalNumber() { 42 | Map> ids = new HashMap>() { 43 | { 44 | put("bM", Arrays.asList(0L)); 45 | put("Uk", Arrays.asList(1L)); 46 | put("gb", Arrays.asList(2L)); 47 | put("Ef", Arrays.asList(3L)); 48 | put("Vq", Arrays.asList(4L)); 49 | put("uw", Arrays.asList(5L)); 50 | put("OI", Arrays.asList(6L)); 51 | put("AX", Arrays.asList(7L)); 52 | put("p6", Arrays.asList(8L)); 53 | put("nJ", Arrays.asList(9L)); 54 | } 55 | }; 56 | for (String id : ids.keySet()) { 57 | List numbers = ids.get(id); 58 | Assertions.assertEquals(sqids.encode(numbers), id); 59 | Assertions.assertEquals(sqids.decode(id), numbers); 60 | } 61 | } 62 | 63 | @Test 64 | public void incrementalNumbers() { 65 | Map> ids = new HashMap>() { 66 | { 67 | put("SvIz", Arrays.asList(0L, 0L)); 68 | put("n3qa", Arrays.asList(0L, 1L)); 69 | put("tryF", Arrays.asList(0L, 2L)); 70 | put("eg6q", Arrays.asList(0L, 3L)); 71 | put("rSCF", Arrays.asList(0L, 4L)); 72 | put("sR8x", Arrays.asList(0L, 5L)); 73 | put("uY2M", Arrays.asList(0L, 6L)); 74 | put("74dI", Arrays.asList(0L, 7L)); 75 | put("30WX", Arrays.asList(0L, 8L)); 76 | put("moxr", Arrays.asList(0L, 9L)); 77 | } 78 | }; 79 | for (String id : ids.keySet()) { 80 | List numbers = ids.get(id); 81 | Assertions.assertEquals(sqids.encode(numbers), id); 82 | Assertions.assertEquals(sqids.decode(id), numbers); 83 | } 84 | } 85 | 86 | @Test 87 | public void multiInput() { 88 | List numbers = Arrays.asList( 89 | 0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L, 90 | 21L, 22L, 23L, 24L, 25L, 26L, 27L, 28L, 29L, 30L, 31L, 32L, 33L, 34L, 35L, 36L, 37L, 38L, 91 | 39L, 40L, 41L, 42L, 43L, 44L, 45L, 46L, 47L, 48L, 49L, 50L, 51L, 52L, 53L, 54L, 55L, 56L, 92 | 57L, 58L, 59L, 60L, 61L, 62L, 63L, 64L, 65L, 66L, 67L, 68L, 69L, 70L, 71L, 72L, 73L, 74L, 93 | 75L, 76L, 77L, 78L, 79L, 80L, 81L, 82L, 83L, 84L, 85L, 86L, 87L, 88L, 89L, 90L, 91L, 92L, 94 | 93L, 94L, 95L, 96L, 97L, 98L, 99L); 95 | Assertions.assertEquals(sqids.decode(sqids.encode(numbers)), numbers); 96 | } 97 | 98 | @Test 99 | public void encodeNoNumbers() { 100 | List numbers = new ArrayList<>(); 101 | Assertions.assertEquals(sqids.encode(numbers), ""); 102 | } 103 | 104 | @Test 105 | public void decodeEmptyString() { 106 | List numbers = new ArrayList<>(); 107 | Assertions.assertEquals(sqids.decode(""), numbers); 108 | } 109 | 110 | @Test 111 | public void decodeInvalidCharacter() { 112 | List numbers = new ArrayList<>(); 113 | Assertions.assertEquals(sqids.decode("*"), numbers); 114 | } 115 | 116 | @Test 117 | public void encodeOutOfRangeNumbers() { 118 | Assertions.assertThrows(RuntimeException.class, () -> sqids.encode(Arrays.asList(-1L))); 119 | Assertions.assertThrows(RuntimeException.class, () -> sqids.encode(Arrays.asList(Long.MAX_VALUE + 1))); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/org/sqids/MinLengthTests.java: -------------------------------------------------------------------------------- 1 | package org.sqids; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class MinLengthTests { 13 | private final int minLength = Sqids.Builder.DEFAULT_ALPHABET.length(); 14 | 15 | @Test 16 | public void simple() { 17 | Sqids sqids = Sqids.builder() 18 | .minLength(minLength) 19 | .build(); 20 | List numbers = Arrays.asList(1L, 2L, 3L); 21 | String id = "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM"; 22 | Assertions.assertEquals(sqids.encode(numbers), id); 23 | Assertions.assertEquals(sqids.decode(id), numbers); 24 | } 25 | 26 | @Test 27 | public void incremental() { 28 | List numbers = Arrays.asList(1L, 2L, 3L); 29 | Map ids = new HashMap() { 30 | { 31 | put(6, "86Rf07"); 32 | put(7, "86Rf07x"); 33 | put(8, "86Rf07xd"); 34 | put(9, "86Rf07xd4"); 35 | put(10, "86Rf07xd4z"); 36 | put(11, "86Rf07xd4zB"); 37 | put(12, "86Rf07xd4zBm"); 38 | put(13, "86Rf07xd4zBmi"); 39 | put(minLength + 0, "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM"); 40 | put(minLength + 1, "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMy"); 41 | put(minLength + 2, "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf"); 42 | put(minLength + 3, "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf1"); 43 | } 44 | }; 45 | for (Integer minLength : ids.keySet()) { 46 | Sqids sqids = Sqids.builder() 47 | .minLength(minLength) 48 | .build(); 49 | String id = ids.get(minLength); 50 | Assertions.assertEquals(sqids.encode(numbers), id); 51 | Assertions.assertEquals(sqids.decode(id), numbers); 52 | } 53 | } 54 | 55 | @Test 56 | public void incrementalNumbers() { 57 | Sqids sqids = Sqids.builder() 58 | .minLength(minLength) 59 | .build(); 60 | Map> ids = new HashMap>() { 61 | { 62 | put("SvIzsqYMyQwI3GWgJAe17URxX8V924Co0DaTZLtFjHriEn5bPhcSkfmvOslpBu", Arrays.asList(0L, 0L)); 63 | put("n3qafPOLKdfHpuNw3M61r95svbeJGk7aAEgYn4WlSjXURmF8IDqZBy0CT2VxQc", Arrays.asList(0L, 1L)); 64 | put("tryFJbWcFMiYPg8sASm51uIV93GXTnvRzyfLleh06CpodJD42B7OraKtkQNxUZ", Arrays.asList(0L, 2L)); 65 | put("eg6ql0A3XmvPoCzMlB6DraNGcWSIy5VR8iYup2Qk4tjZFKe1hbwfgHdUTsnLqE", Arrays.asList(0L, 3L)); 66 | put("rSCFlp0rB2inEljaRdxKt7FkIbODSf8wYgTsZM1HL9JzN35cyoqueUvVWCm4hX", Arrays.asList(0L, 4L)); 67 | put("sR8xjC8WQkOwo74PnglH1YFdTI0eaf56RGVSitzbjuZ3shNUXBrqLxEJyAmKv2", Arrays.asList(0L, 5L)); 68 | put("uY2MYFqCLpgx5XQcjdtZK286AwWV7IBGEfuS9yTmbJvkzoUPeYRHr4iDs3naN0", Arrays.asList(0L, 6L)); 69 | put("74dID7X28VLQhBlnGmjZrec5wTA1fqpWtK4YkaoEIM9SRNiC3gUJH0OFvsPDdy", Arrays.asList(0L, 7L)); 70 | put("30WXpesPhgKiEI5RHTY7xbB1GnytJvXOl2p0AcUjdF6waZDo9Qk8VLzMuWrqCS", Arrays.asList(0L, 8L)); 71 | put("moxr3HqLAK0GsTND6jowfZz3SUx7cQ8aC54Pl1RbIvFXmEJuBMYVeW9yrdOtin", Arrays.asList(0L, 9L)); 72 | } 73 | }; 74 | for (String id : ids.keySet()) { 75 | List numbers = ids.get(id); 76 | Assertions.assertEquals(sqids.encode(numbers), id); 77 | Assertions.assertEquals(sqids.decode(id), numbers); 78 | } 79 | } 80 | 81 | @Test 82 | public void minLengths() { 83 | List minLengths = Arrays.asList(0, 1, 5, 10, minLength); 84 | List> numbers = new ArrayList>() {{ 85 | add(Arrays.asList(0L)); 86 | add(Arrays.asList(0L, 0L, 0L, 0L, 0L)); 87 | add(Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L)); 88 | add(Arrays.asList(100L, 200L, 300L)); 89 | add(Arrays.asList(1000L, 2000L, 30000L)); 90 | add(Arrays.asList((long) Integer.MAX_VALUE)); 91 | }}; 92 | for (Integer minLength : minLengths) { 93 | Sqids sqids = Sqids.builder() 94 | .minLength(minLength) 95 | .build(); 96 | for (List number : numbers) { 97 | String id = sqids.encode(number); 98 | Assertions.assertTrue(id.length() >= minLength); 99 | Assertions.assertEquals(sqids.decode(id), number); 100 | } 101 | } 102 | } 103 | 104 | @Test 105 | public void encodeOutOfRangeNumbers() { 106 | int minLengthLimit = 255; 107 | Assertions.assertThrows(RuntimeException.class, () -> Sqids.builder() 108 | .minLength(-1) 109 | .build()); 110 | Assertions.assertThrows(RuntimeException.class, () -> Sqids.builder() 111 | .minLength(minLengthLimit + 1) 112 | .build()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/org/sqids/Sqids.java: -------------------------------------------------------------------------------- 1 | package org.sqids; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | 12 | /** 13 | * Sqids is designed to generate short YouTube-looking IDs from numbers. 14 | *

15 | * This is the Java implementation of https://github.com/sqids/sqids-spec. 16 | * 17 | * This implementation is immutable and thread-safe, no lock is necessary. 18 | */ 19 | public class Sqids { 20 | /** 21 | * The minimum allowable length of the alphabet used for encoding and 22 | * decoding Sqids. 23 | */ 24 | public static final int MIN_ALPHABET_LENGTH = 3; 25 | 26 | /** 27 | * The maximum allowable minimum length of an encoded Sqid. 28 | */ 29 | public static final int MIN_LENGTH_LIMIT = 255; 30 | 31 | /** 32 | * The minimum length of blocked words in the block list. Any words shorter 33 | * than the minimum are ignored. 34 | */ 35 | public static final int MIN_BLOCK_LIST_WORD_LENGTH = 3; 36 | 37 | private final String alphabet; 38 | private final int alphabetLength; 39 | private final int minLength; 40 | private final Set blockList; 41 | 42 | private Sqids(final Builder builder) { 43 | final String alphabet = builder.alphabet; 44 | final int alphabetLength = alphabet.length(); 45 | final int minLength = builder.minLength; 46 | final Set blockList = new HashSet<>(builder.blockList); 47 | 48 | if (alphabet.getBytes().length != alphabetLength) { 49 | throw new IllegalArgumentException("Alphabet cannot contain multibyte characters"); 50 | } 51 | 52 | if (alphabetLength < MIN_ALPHABET_LENGTH) { 53 | throw new IllegalArgumentException("Alphabet length must be at least " + MIN_ALPHABET_LENGTH); 54 | } 55 | 56 | if (new HashSet<>(Arrays.asList(alphabet.split(""))).size() != alphabetLength) { 57 | throw new IllegalArgumentException("Alphabet must contain unique characters"); 58 | } 59 | 60 | if (minLength < 0 || minLength > MIN_LENGTH_LIMIT) { 61 | throw new IllegalArgumentException("Minimum length has to be between 0 and " + MIN_LENGTH_LIMIT); 62 | } 63 | 64 | final Set filteredBlockList = new HashSet<>(); 65 | final List alphabetChars = new ArrayList<>(Arrays.asList(alphabet.toLowerCase().split(""))); 66 | for (String word : blockList) { 67 | if (word.length() >= MIN_BLOCK_LIST_WORD_LENGTH) { 68 | word = word.toLowerCase(); 69 | List wordChars = Arrays.asList(word.split("")); 70 | List intersection = new ArrayList<>(wordChars); 71 | intersection.retainAll(alphabetChars); 72 | if (intersection.size() == wordChars.size()) { 73 | filteredBlockList.add(word); 74 | } 75 | } 76 | } 77 | 78 | this.alphabet = this.shuffle(alphabet); 79 | this.alphabetLength = this.alphabet.length(); 80 | this.minLength = minLength; 81 | this.blockList = filteredBlockList; 82 | } 83 | 84 | /** 85 | * Generate a Sqids' Builder. 86 | * 87 | * @return New Builder instance. 88 | */ 89 | public static Builder builder() { 90 | return new Builder(); 91 | } 92 | 93 | /** 94 | * Encode a list of numbers to a Sqids ID. 95 | * 96 | * @param numbers Numbers to encode. 97 | * @return Sqids ID. 98 | */ 99 | public String encode(final List numbers) { 100 | if (numbers.isEmpty()) { 101 | return ""; 102 | } 103 | for (Long num : numbers) { 104 | if (num < 0) { 105 | throw new RuntimeException("Encoding supports numbers between 0 and " + Long.MAX_VALUE); 106 | } 107 | } 108 | return encodeNumbers(numbers); 109 | } 110 | 111 | /** 112 | * Decode a Sqids ID back to numbers. 113 | * 114 | * @param id ID to decode. 115 | * @return List of decoded numbers. 116 | */ 117 | public List decode(final String id) { 118 | List ret = new ArrayList<>(); 119 | if (id.isEmpty()) { 120 | return ret; 121 | } 122 | 123 | final char[] alphabetChars = this.alphabet.toCharArray(); 124 | Set alphabetSet = new HashSet<>(); 125 | for (final char c : alphabetChars) { 126 | alphabetSet.add(c); 127 | } 128 | for (final char c : id.toCharArray()) { 129 | if (!alphabetSet.contains(c)) { 130 | return ret; 131 | } 132 | } 133 | 134 | final char prefix = id.charAt(0); 135 | final int offset = this.alphabet.indexOf(prefix); 136 | String alphabet = new StringBuilder(this.alphabet.substring(offset)) 137 | .append(this.alphabet, 0, offset) 138 | .reverse() 139 | .toString(); 140 | 141 | int index = 1; 142 | while (true) { 143 | final char separator = alphabet.charAt(0); 144 | int separatorIndex = id.indexOf(separator, index); 145 | if (separatorIndex == -1) { 146 | separatorIndex = id.length(); 147 | } else if (index == separatorIndex) { 148 | break; 149 | } 150 | ret.add(toNumber(id, index, separatorIndex, alphabet.substring(1))); 151 | index = separatorIndex + 1; 152 | if (index < id.length()) { 153 | alphabet = shuffle(alphabet); 154 | } else { 155 | break; 156 | } 157 | } 158 | return ret; 159 | } 160 | 161 | private String encodeNumbers(final List numbers) { 162 | return this.encodeNumbers(numbers, 0); 163 | } 164 | 165 | private String encodeNumbers(final List numbers, final int increment) { 166 | if (increment > this.alphabetLength) { 167 | throw new RuntimeException("Reached max attempts to re-generate the ID"); 168 | } 169 | 170 | final int numberSize = numbers.size(); 171 | long offset = numberSize; 172 | for (int i = 0; i < numberSize; i++) { 173 | offset = offset + this.alphabet.charAt((int) (numbers.get(i) % this.alphabetLength)) + i; 174 | } 175 | offset %= this.alphabetLength; 176 | offset = (offset + increment) % this.alphabetLength; 177 | 178 | final StringBuilder alphabetB = new StringBuilder(this.alphabet.substring((int) offset)) 179 | .append(this.alphabet, 0, (int) offset); 180 | final char prefix = alphabetB.charAt(0); 181 | String alphabet = alphabetB.reverse().toString(); 182 | final StringBuilder id = new StringBuilder().append(prefix); 183 | for (int i = 0; i < numberSize; i++) { 184 | final long num = numbers.get(i); 185 | id.append(toId(num, alphabet.substring(1))); 186 | if (i < numberSize - 1) { 187 | id.append(alphabet.charAt(0)); 188 | alphabet = shuffle(alphabet); 189 | } 190 | } 191 | 192 | if (this.minLength > id.length()) { 193 | id.append(alphabet.charAt(0)); 194 | while (this.minLength - id.length() > 0) { 195 | alphabet = shuffle(alphabet); 196 | id.append(alphabet, 0, Math.min(this.minLength - id.length(), alphabet.length())); 197 | } 198 | } 199 | 200 | if (isBlockedId(id.toString())) { 201 | id.setLength(0); 202 | id.append(encodeNumbers(numbers, increment + 1)); 203 | } 204 | 205 | return id.toString(); 206 | } 207 | 208 | private String shuffle(final String alphabet) { 209 | char[] chars = alphabet.toCharArray(); 210 | int charLength = chars.length; 211 | for (int i = 0, j = charLength - 1; j > 0; i++, j--) { 212 | int r = (i * j + chars[i] + chars[j]) % charLength; 213 | char temp = chars[i]; 214 | chars[i] = chars[r]; 215 | chars[r] = temp; 216 | } 217 | 218 | return new String(chars); 219 | } 220 | 221 | private StringBuilder toId(long num, final String alphabet) { 222 | StringBuilder id = new StringBuilder(); 223 | char[] chars = alphabet.toCharArray(); 224 | int charLength = chars.length; 225 | 226 | do { 227 | id.append(chars[(int) (num % charLength)]); 228 | num /= charLength; 229 | } while (num > 0); 230 | 231 | return id.reverse(); 232 | } 233 | 234 | private long toNumber(final String id, final int fromInclusive, final int toExclusive, final String alphabet) { 235 | int alphabetLength = alphabet.length(); 236 | long number = 0; 237 | for (int i = fromInclusive; i < toExclusive; i++) { 238 | char c = id.charAt(i); 239 | number = number * alphabetLength + alphabet.indexOf(c); 240 | } 241 | return number; 242 | } 243 | 244 | private boolean isBlockedId(final String id) { 245 | final String lowercaseId = id.toLowerCase(); 246 | final int lowercaseIdLength = lowercaseId.length(); 247 | for (String word : this.blockList) { 248 | if (word.length() <= lowercaseIdLength) { 249 | if (lowercaseIdLength <= 3 || word.length() <= 3) { 250 | if (lowercaseId.equals(word)) { 251 | return true; 252 | } 253 | } else if (Character.isDigit(word.charAt(0))) { 254 | if (lowercaseId.startsWith(word) || lowercaseId.endsWith(word)) { 255 | return true; 256 | } 257 | } else if (lowercaseId.contains(word)) { 258 | return true; 259 | } 260 | } 261 | } 262 | return false; 263 | } 264 | 265 | /** 266 | * Default Sqids' {@code Builder}. 267 | */ 268 | public static final class Builder { 269 | /** 270 | * Default Alphabet used by {@code Builder}. 271 | */ 272 | public static final String DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 273 | 274 | /** 275 | * Default Minimum length used by {@code Builder}. 276 | */ 277 | public static final int DEFAULT_MIN_LENGTH = 0; 278 | 279 | /** 280 | * Default Block list used by {@code Builder}. 281 | * 282 | * Note: This is a Immutable Set. 283 | */ 284 | public static final Set DEFAULT_BLOCK_LIST = Collections.unmodifiableSet(Stream.of( 285 | "1d10t", 286 | "1d1ot", 287 | "1di0t", 288 | "1diot", 289 | "1eccacu10", 290 | "1eccacu1o", 291 | "1eccacul0", 292 | "1eccaculo", 293 | "1mbec11e", 294 | "1mbec1le", 295 | "1mbeci1e", 296 | "1mbecile", 297 | "a11upat0", 298 | "a11upato", 299 | "a1lupat0", 300 | "a1lupato", 301 | "aand", 302 | "ah01e", 303 | "ah0le", 304 | "aho1e", 305 | "ahole", 306 | "al1upat0", 307 | "al1upato", 308 | "allupat0", 309 | "allupato", 310 | "ana1", 311 | "ana1e", 312 | "anal", 313 | "anale", 314 | "anus", 315 | "arrapat0", 316 | "arrapato", 317 | "arsch", 318 | "arse", 319 | "ass", 320 | "b00b", 321 | "b00be", 322 | "b01ata", 323 | "b0ceta", 324 | "b0iata", 325 | "b0ob", 326 | "b0obe", 327 | "b0sta", 328 | "b1tch", 329 | "b1te", 330 | "b1tte", 331 | "ba1atkar", 332 | "balatkar", 333 | "bastard0", 334 | "bastardo", 335 | "batt0na", 336 | "battona", 337 | "bitch", 338 | "bite", 339 | "bitte", 340 | "bo0b", 341 | "bo0be", 342 | "bo1ata", 343 | "boceta", 344 | "boiata", 345 | "boob", 346 | "boobe", 347 | "bosta", 348 | "bran1age", 349 | "bran1er", 350 | "bran1ette", 351 | "bran1eur", 352 | "bran1euse", 353 | "branlage", 354 | "branler", 355 | "branlette", 356 | "branleur", 357 | "branleuse", 358 | "c0ck", 359 | "c0g110ne", 360 | "c0g11one", 361 | "c0g1i0ne", 362 | "c0g1ione", 363 | "c0gl10ne", 364 | "c0gl1one", 365 | "c0gli0ne", 366 | "c0glione", 367 | "c0na", 368 | "c0nnard", 369 | "c0nnasse", 370 | "c0nne", 371 | "c0u111es", 372 | "c0u11les", 373 | "c0u1l1es", 374 | "c0u1lles", 375 | "c0ui11es", 376 | "c0ui1les", 377 | "c0uil1es", 378 | "c0uilles", 379 | "c11t", 380 | "c11t0", 381 | "c11to", 382 | "c1it", 383 | "c1it0", 384 | "c1ito", 385 | "cabr0n", 386 | "cabra0", 387 | "cabrao", 388 | "cabron", 389 | "caca", 390 | "cacca", 391 | "cacete", 392 | "cagante", 393 | "cagar", 394 | "cagare", 395 | "cagna", 396 | "cara1h0", 397 | "cara1ho", 398 | "caracu10", 399 | "caracu1o", 400 | "caracul0", 401 | "caraculo", 402 | "caralh0", 403 | "caralho", 404 | "cazz0", 405 | "cazz1mma", 406 | "cazzata", 407 | "cazzimma", 408 | "cazzo", 409 | "ch00t1a", 410 | "ch00t1ya", 411 | "ch00tia", 412 | "ch00tiya", 413 | "ch0d", 414 | "ch0ot1a", 415 | "ch0ot1ya", 416 | "ch0otia", 417 | "ch0otiya", 418 | "ch1asse", 419 | "ch1avata", 420 | "ch1er", 421 | "ch1ng0", 422 | "ch1ngadaz0s", 423 | "ch1ngadazos", 424 | "ch1ngader1ta", 425 | "ch1ngaderita", 426 | "ch1ngar", 427 | "ch1ngo", 428 | "ch1ngues", 429 | "ch1nk", 430 | "chatte", 431 | "chiasse", 432 | "chiavata", 433 | "chier", 434 | "ching0", 435 | "chingadaz0s", 436 | "chingadazos", 437 | "chingader1ta", 438 | "chingaderita", 439 | "chingar", 440 | "chingo", 441 | "chingues", 442 | "chink", 443 | "cho0t1a", 444 | "cho0t1ya", 445 | "cho0tia", 446 | "cho0tiya", 447 | "chod", 448 | "choot1a", 449 | "choot1ya", 450 | "chootia", 451 | "chootiya", 452 | "cl1t", 453 | "cl1t0", 454 | "cl1to", 455 | "clit", 456 | "clit0", 457 | "clito", 458 | "cock", 459 | "cog110ne", 460 | "cog11one", 461 | "cog1i0ne", 462 | "cog1ione", 463 | "cogl10ne", 464 | "cogl1one", 465 | "cogli0ne", 466 | "coglione", 467 | "cona", 468 | "connard", 469 | "connasse", 470 | "conne", 471 | "cou111es", 472 | "cou11les", 473 | "cou1l1es", 474 | "cou1lles", 475 | "coui11es", 476 | "coui1les", 477 | "couil1es", 478 | "couilles", 479 | "cracker", 480 | "crap", 481 | "cu10", 482 | "cu1att0ne", 483 | "cu1attone", 484 | "cu1er0", 485 | "cu1ero", 486 | "cu1o", 487 | "cul0", 488 | "culatt0ne", 489 | "culattone", 490 | "culer0", 491 | "culero", 492 | "culo", 493 | "cum", 494 | "cunt", 495 | "d11d0", 496 | "d11do", 497 | "d1ck", 498 | "d1ld0", 499 | "d1ldo", 500 | "damn", 501 | "de1ch", 502 | "deich", 503 | "depp", 504 | "di1d0", 505 | "di1do", 506 | "dick", 507 | "dild0", 508 | "dildo", 509 | "dyke", 510 | "encu1e", 511 | "encule", 512 | "enema", 513 | "enf01re", 514 | "enf0ire", 515 | "enfo1re", 516 | "enfoire", 517 | "estup1d0", 518 | "estup1do", 519 | "estupid0", 520 | "estupido", 521 | "etr0n", 522 | "etron", 523 | "f0da", 524 | "f0der", 525 | "f0ttere", 526 | "f0tters1", 527 | "f0ttersi", 528 | "f0tze", 529 | "f0utre", 530 | "f1ca", 531 | "f1cker", 532 | "f1ga", 533 | "fag", 534 | "fica", 535 | "ficker", 536 | "figa", 537 | "foda", 538 | "foder", 539 | "fottere", 540 | "fotters1", 541 | "fottersi", 542 | "fotze", 543 | "foutre", 544 | "fr0c10", 545 | "fr0c1o", 546 | "fr0ci0", 547 | "fr0cio", 548 | "fr0sc10", 549 | "fr0sc1o", 550 | "fr0sci0", 551 | "fr0scio", 552 | "froc10", 553 | "froc1o", 554 | "froci0", 555 | "frocio", 556 | "frosc10", 557 | "frosc1o", 558 | "frosci0", 559 | "froscio", 560 | "fuck", 561 | "g00", 562 | "g0o", 563 | "g0u1ne", 564 | "g0uine", 565 | "gandu", 566 | "go0", 567 | "goo", 568 | "gou1ne", 569 | "gouine", 570 | "gr0gnasse", 571 | "grognasse", 572 | "haram1", 573 | "harami", 574 | "haramzade", 575 | "hund1n", 576 | "hundin", 577 | "id10t", 578 | "id1ot", 579 | "idi0t", 580 | "idiot", 581 | "imbec11e", 582 | "imbec1le", 583 | "imbeci1e", 584 | "imbecile", 585 | "j1zz", 586 | "jerk", 587 | "jizz", 588 | "k1ke", 589 | "kam1ne", 590 | "kamine", 591 | "kike", 592 | "leccacu10", 593 | "leccacu1o", 594 | "leccacul0", 595 | "leccaculo", 596 | "m1erda", 597 | "m1gn0tta", 598 | "m1gnotta", 599 | "m1nch1a", 600 | "m1nchia", 601 | "m1st", 602 | "mam0n", 603 | "mamahuev0", 604 | "mamahuevo", 605 | "mamon", 606 | "masturbat10n", 607 | "masturbat1on", 608 | "masturbate", 609 | "masturbati0n", 610 | "masturbation", 611 | "merd0s0", 612 | "merd0so", 613 | "merda", 614 | "merde", 615 | "merdos0", 616 | "merdoso", 617 | "mierda", 618 | "mign0tta", 619 | "mignotta", 620 | "minch1a", 621 | "minchia", 622 | "mist", 623 | "musch1", 624 | "muschi", 625 | "n1gger", 626 | "neger", 627 | "negr0", 628 | "negre", 629 | "negro", 630 | "nerch1a", 631 | "nerchia", 632 | "nigger", 633 | "orgasm", 634 | "p00p", 635 | "p011a", 636 | "p01la", 637 | "p0l1a", 638 | "p0lla", 639 | "p0mp1n0", 640 | "p0mp1no", 641 | "p0mpin0", 642 | "p0mpino", 643 | "p0op", 644 | "p0rca", 645 | "p0rn", 646 | "p0rra", 647 | "p0uff1asse", 648 | "p0uffiasse", 649 | "p1p1", 650 | "p1pi", 651 | "p1r1a", 652 | "p1rla", 653 | "p1sc10", 654 | "p1sc1o", 655 | "p1sci0", 656 | "p1scio", 657 | "p1sser", 658 | "pa11e", 659 | "pa1le", 660 | "pal1e", 661 | "palle", 662 | "pane1e1r0", 663 | "pane1e1ro", 664 | "pane1eir0", 665 | "pane1eiro", 666 | "panele1r0", 667 | "panele1ro", 668 | "paneleir0", 669 | "paneleiro", 670 | "patakha", 671 | "pec0r1na", 672 | "pec0rina", 673 | "pecor1na", 674 | "pecorina", 675 | "pen1s", 676 | "pendej0", 677 | "pendejo", 678 | "penis", 679 | "pip1", 680 | "pipi", 681 | "pir1a", 682 | "pirla", 683 | "pisc10", 684 | "pisc1o", 685 | "pisci0", 686 | "piscio", 687 | "pisser", 688 | "po0p", 689 | "po11a", 690 | "po1la", 691 | "pol1a", 692 | "polla", 693 | "pomp1n0", 694 | "pomp1no", 695 | "pompin0", 696 | "pompino", 697 | "poop", 698 | "porca", 699 | "porn", 700 | "porra", 701 | "pouff1asse", 702 | "pouffiasse", 703 | "pr1ck", 704 | "prick", 705 | "pussy", 706 | "put1za", 707 | "puta", 708 | "puta1n", 709 | "putain", 710 | "pute", 711 | "putiza", 712 | "puttana", 713 | "queca", 714 | "r0mp1ba11e", 715 | "r0mp1ba1le", 716 | "r0mp1bal1e", 717 | "r0mp1balle", 718 | "r0mpiba11e", 719 | "r0mpiba1le", 720 | "r0mpibal1e", 721 | "r0mpiballe", 722 | "rand1", 723 | "randi", 724 | "rape", 725 | "recch10ne", 726 | "recch1one", 727 | "recchi0ne", 728 | "recchione", 729 | "retard", 730 | "romp1ba11e", 731 | "romp1ba1le", 732 | "romp1bal1e", 733 | "romp1balle", 734 | "rompiba11e", 735 | "rompiba1le", 736 | "rompibal1e", 737 | "rompiballe", 738 | "ruff1an0", 739 | "ruff1ano", 740 | "ruffian0", 741 | "ruffiano", 742 | "s1ut", 743 | "sa10pe", 744 | "sa1aud", 745 | "sa1ope", 746 | "sacanagem", 747 | "sal0pe", 748 | "salaud", 749 | "salope", 750 | "saugnapf", 751 | "sb0rr0ne", 752 | "sb0rra", 753 | "sb0rrone", 754 | "sbattere", 755 | "sbatters1", 756 | "sbattersi", 757 | "sborr0ne", 758 | "sborra", 759 | "sborrone", 760 | "sc0pare", 761 | "sc0pata", 762 | "sch1ampe", 763 | "sche1se", 764 | "sche1sse", 765 | "scheise", 766 | "scheisse", 767 | "schlampe", 768 | "schwachs1nn1g", 769 | "schwachs1nnig", 770 | "schwachsinn1g", 771 | "schwachsinnig", 772 | "schwanz", 773 | "scopare", 774 | "scopata", 775 | "sexy", 776 | "sh1t", 777 | "shit", 778 | "slut", 779 | "sp0mp1nare", 780 | "sp0mpinare", 781 | "spomp1nare", 782 | "spompinare", 783 | "str0nz0", 784 | "str0nza", 785 | "str0nzo", 786 | "stronz0", 787 | "stronza", 788 | "stronzo", 789 | "stup1d", 790 | "stupid", 791 | "succh1am1", 792 | "succh1ami", 793 | "succhiam1", 794 | "succhiami", 795 | "sucker", 796 | "t0pa", 797 | "tapette", 798 | "test1c1e", 799 | "test1cle", 800 | "testic1e", 801 | "testicle", 802 | "tette", 803 | "topa", 804 | "tr01a", 805 | "tr0ia", 806 | "tr0mbare", 807 | "tr1ng1er", 808 | "tr1ngler", 809 | "tring1er", 810 | "tringler", 811 | "tro1a", 812 | "troia", 813 | "trombare", 814 | "turd", 815 | "twat", 816 | "vaffancu10", 817 | "vaffancu1o", 818 | "vaffancul0", 819 | "vaffanculo", 820 | "vag1na", 821 | "vagina", 822 | "verdammt", 823 | "verga", 824 | "w1chsen", 825 | "wank", 826 | "wichsen", 827 | "x0ch0ta", 828 | "x0chota", 829 | "xana", 830 | "xoch0ta", 831 | "xochota", 832 | "z0cc01a", 833 | "z0cc0la", 834 | "z0cco1a", 835 | "z0ccola", 836 | "z1z1", 837 | "z1zi", 838 | "ziz1", 839 | "zizi", 840 | "zocc01a", 841 | "zocc0la", 842 | "zocco1a", 843 | "zoccola").collect(Collectors.toSet())); 844 | 845 | private String alphabet = DEFAULT_ALPHABET; 846 | private int minLength = DEFAULT_MIN_LENGTH; 847 | private Set blockList = DEFAULT_BLOCK_LIST; 848 | 849 | /** 850 | * Set {@code Builder}'s alphabet. 851 | * 852 | * @param alphabet The new {@code Builder}'s alphabet 853 | * @return this {@code Builder} object 854 | */ 855 | public Builder alphabet(final String alphabet) { 856 | if (alphabet != null) { 857 | this.alphabet = alphabet; 858 | } 859 | return this; 860 | } 861 | 862 | /** 863 | * Set {@code Builder}'s minimum length. 864 | * 865 | * @param minLength The new {@code Builder}'s minimum length. 866 | * @return this {@code Builder} object 867 | */ 868 | public Builder minLength(final int minLength) { 869 | this.minLength = minLength; 870 | return this; 871 | } 872 | 873 | /** 874 | * Set {@code Builder}'s block list. 875 | * 876 | * @param blockList The new {@code Builder}'s block list. A copy will be created. 877 | * @return this {@code Builder} object 878 | */ 879 | public Builder blockList(final Set blockList) { 880 | if (blockList != null) { 881 | this.blockList = Collections.unmodifiableSet(new HashSet<>(blockList)); 882 | } 883 | return this; 884 | } 885 | 886 | /** 887 | * Returns a newly-created {@code Sqids} based on the contents of this {@code Builder}. 888 | * 889 | * @return New Sqids instance. 890 | */ 891 | public Sqids build() { 892 | return new Sqids(this); 893 | } 894 | } 895 | } 896 | --------------------------------------------------------------------------------