| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.nlpcraft.common.util; |
| |
| import java.util.*; |
| |
| /** |
| * Implementation is based on https://github.com/peet/hashids.java/blob/master/src/HashidsJava/Hashids.java |
| */ |
| public class NCIdGenerator { |
| private static final String DEFAULT_ALPHABET = "xcS4F6h89aUbideAI7tkynuopqrXCgTE5GBKHLMjfRsz"; |
| private static final int[] PRIMES = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43}; |
| private static final int[] SEPS_INDICES = {0, 4, 8, 12}; |
| |
| private String alphabet; |
| private int minHashLen; |
| |
| private String salt = ""; |
| |
| private final ArrayList<Character> seps = new ArrayList<>(); |
| private final ArrayList<Character> guards = new ArrayList<>(); |
| |
| /** |
| * |
| * @param salt Salt seed. |
| * @param minHashLen Minimal hash length. |
| */ |
| NCIdGenerator(String salt, int minHashLen) { |
| this(salt, minHashLen, DEFAULT_ALPHABET); |
| } |
| |
| /** |
| * |
| * @param salt Salt seed. |
| * @param minHashLen Minimal hash length. |
| * @param alphabet Alphabet. |
| */ |
| NCIdGenerator(String salt, int minHashLen, String alphabet) { |
| if (alphabet == null || alphabet.strip().isEmpty()) |
| throw new IllegalArgumentException("Alphabet must not be empty."); |
| |
| if (salt != null) |
| this.salt = salt; |
| |
| if (minHashLen > 0) |
| this.minHashLen = minHashLen; |
| |
| this.alphabet = join(new LinkedHashSet<>(Arrays.asList(alphabet.split(""))), ""); |
| |
| if (this.alphabet.length() < 4) |
| throw new IllegalArgumentException("Alphabet must contain at least 4 unique characters."); |
| |
| for (int prime : PRIMES) |
| if (prime < this.alphabet.length()) { |
| char c = this.alphabet.charAt(prime - 1); |
| seps.add(c); |
| this.alphabet = this.alphabet.replace(c, ' '); |
| } |
| |
| for (int index : SEPS_INDICES) |
| if (index < seps.size()) { |
| guards.add(seps.get(index)); |
| seps.remove(index); |
| } |
| |
| this.alphabet = consistentShuffle(this.alphabet.replaceAll(" ", ""), this.salt); |
| } |
| |
| /** |
| * Encrypts one or more long values. |
| * |
| * @param nums Long values to encrypt. |
| * @return Encrypted value (hash). |
| * @see #decrypt(String) |
| */ |
| public String encrypt(long... nums) { |
| return encode(nums, alphabet, salt, minHashLen); |
| } |
| |
| /** |
| * Decrypts given hash into array of long. |
| * |
| * @param hash Hash value to decrypt. |
| * @return Array of long values. |
| * @see #encrypt(long...) |
| */ |
| public long[] decrypt(String hash) { |
| return decode(hash); |
| } |
| |
| /** |
| * |
| * @param nums |
| * @param alphabet |
| * @param salt |
| * @param minHashLen |
| * @return |
| */ |
| private String encode(long[] nums, String alphabet, String salt, int minHashLen) { |
| StringBuilder ret = new StringBuilder(); |
| String seps = consistentShuffle(join(this.seps, ""), join(nums, "")); |
| char lotteryChar = 0; |
| |
| for (int i = 0; i < nums.length; i++) { |
| if (i == 0) { |
| StringBuilder lotterySalt = new StringBuilder(join(nums, "-")); |
| |
| for (long number : nums) |
| lotterySalt.append("-").append((number + 1) * 2); |
| |
| String lottery = consistentShuffle(alphabet, lotterySalt.toString()); |
| |
| lotteryChar = lottery.charAt(0); |
| |
| ret.append(lotteryChar); |
| |
| alphabet = lotteryChar + alphabet.replaceAll(String.valueOf(lotteryChar), ""); |
| } |
| |
| alphabet = consistentShuffle(alphabet, ((int) lotteryChar & 12345) + salt); |
| |
| ret.append(hash(nums[i], alphabet)); |
| |
| if (i + 1 < nums.length) |
| ret.append(seps.charAt((int) ((nums[i] + i) % seps.length()))); |
| } |
| |
| if (ret.length() < minHashLen) { |
| int firstIndex = 0; |
| |
| for (int i = 0; i < nums.length; i++) |
| firstIndex += (i + 1) * nums[i]; |
| |
| int guardIndex = firstIndex % guards.size(); |
| char guard = guards.get(guardIndex); |
| |
| ret.insert(0, guard); |
| |
| if (ret.length() < minHashLen) { |
| guardIndex = (guardIndex + ret.length()) % guards.size(); |
| guard = guards.get(guardIndex); |
| |
| ret.append(guard); |
| } |
| } |
| |
| while (ret.length() < minHashLen) { |
| long[] padArray = new long[]{alphabet.charAt(1), alphabet.charAt(0)}; |
| String padLeft = encode(padArray, alphabet, salt, 0); |
| String padRight = encode(padArray, alphabet, join(padArray, ""), 0); |
| |
| ret = new StringBuilder(padLeft + ret + padRight); |
| |
| int excess = ret.length() - minHashLen; |
| |
| if (excess > 0) |
| ret = new StringBuilder(ret.substring(excess / 2, excess / 2 + minHashLen)); |
| |
| alphabet = consistentShuffle(alphabet, salt + ret); |
| } |
| |
| return ret.toString(); |
| } |
| |
| /** |
| * |
| * @param number |
| * @param alphabet |
| * @return |
| */ |
| private String hash(long number, String alphabet) { |
| StringBuilder hash = new StringBuilder(); |
| |
| while (number > 0) { |
| hash.insert(0, alphabet.charAt((int) (number % alphabet.length()))); |
| |
| number = number / alphabet.length(); |
| } |
| |
| return hash.toString(); |
| } |
| |
| /** |
| * |
| * @param hash Hash to un-hash. |
| * @param alphabet Alphabet to use. |
| * @return Unhashed long value. |
| */ |
| private long unhash(String hash, String alphabet) { |
| long num = 0; |
| |
| for (int i = 0; i < hash.length(); i++) { |
| int pos = alphabet.indexOf(hash.charAt(i)); |
| |
| num += pos * (long) Math.pow(alphabet.length(), hash.length() - i - 1); |
| } |
| |
| return num; |
| } |
| |
| /** |
| * |
| * @param hash Hash to decode. |
| * @return Array of longs. |
| */ |
| private long[] decode(String hash) { |
| List<Long> ret = new ArrayList<>(); |
| String originalHash = hash; |
| |
| if (hash != null && !hash.isEmpty()) { |
| String alphabet = ""; |
| |
| char lotteryChar = 0; |
| |
| for (char guard : guards) |
| hash = hash.replaceAll(String.valueOf(guard), " "); |
| |
| String[] hashSplit = hash.split(" "); |
| |
| hash = hashSplit[hashSplit.length == 3 || hashSplit.length == 2 ? 1 : 0]; |
| |
| for (char sep : seps) |
| hash = hash.replaceAll(String.valueOf(sep), " "); |
| |
| String[] hashArray = hash.split(" "); |
| |
| for (int i = 0; i < hashArray.length; i++) { |
| String subHash = hashArray[i]; |
| |
| if (subHash != null && !subHash.isEmpty() && i == 0) { |
| lotteryChar = hash.charAt(0); |
| |
| subHash = subHash.substring(1); |
| |
| alphabet = lotteryChar + this.alphabet.replaceAll(String.valueOf(lotteryChar), ""); |
| } |
| |
| if (alphabet.length() > 0) { |
| assert subHash != null; |
| |
| alphabet = consistentShuffle(alphabet, ((int) lotteryChar & 12345) + salt); |
| |
| ret.add(unhash(subHash, alphabet)); |
| } |
| } |
| } |
| |
| long[] nums = longListToPrimitiveArray(ret); |
| |
| if (!encrypt(nums).equals(originalHash)) |
| return new long[0]; |
| |
| return nums; |
| } |
| |
| /** |
| * |
| * @param alphabet Alphabet to use. |
| * @param salt Salt seed. |
| * @return Shuffle result. |
| */ |
| private static String consistentShuffle(String alphabet, String salt) { |
| StringBuilder ret = new StringBuilder(); |
| |
| if (!alphabet.isEmpty()) { |
| List<String> alphabetArr = charArrayToStringList(alphabet.toCharArray()); |
| |
| if (salt == null || salt.isEmpty()) |
| salt = new String(new char[]{'\0'}); |
| |
| int[] sortArr = new int[salt.length()]; |
| |
| for (int i = 0; i < salt.length(); i++) |
| sortArr[i] = salt.charAt(i); |
| |
| for (int i = 0; i < sortArr.length; i++) { |
| boolean add = true; |
| |
| for (int k = i; k != sortArr.length + i - 1; k++) { |
| int nextIndex = (k + 1) % sortArr.length; |
| |
| if (add) |
| sortArr[i] += sortArr[nextIndex] + (k * i); |
| else |
| sortArr[i] -= sortArr[nextIndex]; |
| |
| add = !add; |
| } |
| |
| sortArr[i] = Math.abs(sortArr[i]); |
| } |
| |
| int i = 0; |
| |
| while (alphabetArr.size() > 0) { |
| int pos = sortArr[i]; |
| |
| if (pos >= alphabetArr.size()) |
| pos %= alphabetArr.size(); |
| |
| ret.append(alphabetArr.get(pos)); |
| |
| alphabetArr.remove(pos); |
| |
| i = ++i % sortArr.length; |
| } |
| } |
| |
| return ret.toString(); |
| } |
| |
| /** |
| * |
| * @return Salt seed. |
| */ |
| public String getSalt() { |
| return salt; |
| } |
| |
| /** |
| * |
| * @return Alphabet in use. |
| */ |
| public String getAlphabet() { |
| return alphabet; |
| } |
| |
| /** |
| * |
| * @return Minimal hash length. |
| */ |
| public int getMinHashLength() { |
| return minHashLen; |
| } |
| |
| /** |
| * |
| * @param longs List to map. |
| * @return Mapped list of longs. |
| */ |
| private static long[] longListToPrimitiveArray(List<Long> longs) { |
| long[] longArr = new long[longs.size()]; |
| |
| int i = 0; |
| |
| for (long l : longs) |
| longArr[i++] = l; |
| |
| return longArr; |
| } |
| |
| /** |
| * |
| * @param chars Characters to map. |
| * @return List of strings. |
| */ |
| private static List<String> charArrayToStringList(char[] chars) { |
| ArrayList<String> lst = new ArrayList<>(chars.length); |
| |
| for (char c : chars) |
| lst.add(String.valueOf(c)); |
| |
| return lst; |
| } |
| |
| /** |
| * |
| * @param a |
| * @param del |
| * @return |
| */ |
| private static String join(long[] a, String del) { |
| ArrayList<String> strLst = new ArrayList<>(a.length); |
| |
| for (long l : a) |
| strLst.add(String.valueOf(l)); |
| |
| return join(strLst, del); |
| } |
| |
| /** |
| * |
| * @param s |
| * @param del |
| * @return |
| */ |
| private static String join(Collection<?> s, String del) { |
| Iterator<?> iter = s.iterator(); |
| |
| if (iter.hasNext()) { |
| StringBuilder builder = new StringBuilder(s.size()); |
| |
| builder.append(iter.next()); |
| |
| while (iter.hasNext()) { |
| builder.append(del); |
| builder.append(iter.next()); |
| } |
| |
| return builder.toString(); |
| } |
| |
| return ""; |
| } |
| } |