package com.blade.kit;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Hashids designed for Generating short hashes from numbers (like YouTube and
* Bitly), obfuscate database IDs, use them as forgotten password hashes,
* invitation codes, store shard numbers This is implementation of
* http://hashids.org v0.3.3 version.
*
* @author <a href="mailto:fanweixiao@gmail.com">fanweixiao</a>
* @since 0.3.3
*/
public class HashidKit {
private static final String DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
private String salt = "";
private String alphabet = "";
private String seps = "cfhistuCFHISTU";
private int minHashLength = 0;
private String guards;
public HashidKit() {
this("");
}
public HashidKit(String salt) {
this(salt, 0);
}
public HashidKit(String salt, int minHashLength) {
this(salt, minHashLength, DEFAULT_ALPHABET);
}
public HashidKit(String salt, int minHashLength, String alphabet) {
this.salt = salt;
if (minHashLength < 0)
this.minHashLength = 0;
else
this.minHashLength = minHashLength;
this.alphabet = alphabet;
String uniqueAlphabet = "";
for (int i = 0; i < this.alphabet.length(); i++) {
if (!uniqueAlphabet.contains("" + this.alphabet.charAt(i))) {
uniqueAlphabet += "" + this.alphabet.charAt(i);
}
}
this.alphabet = uniqueAlphabet;
int minAlphabetLength = 16;
if (this.alphabet.length() < minAlphabetLength) {
throw new IllegalArgumentException(
"alphabet must contain at least " + minAlphabetLength
+ " unique characters");
}
if (this.alphabet.contains(" ")) {
throw new IllegalArgumentException(
"alphabet cannot contains spaces");
}
// seps should contain only characters present in alphabet;
// alphabet should not contains seps
for (int i = 0; i < this.seps.length(); i++) {
int j = this.alphabet.indexOf(this.seps.charAt(i));
if (j == -1) {
this.seps = this.seps.substring(0, i) + " "
+ this.seps.substring(i + 1);
} else {
this.alphabet = this.alphabet.substring(0, j) + " "
+ this.alphabet.substring(j + 1);
}
}
this.alphabet = this.alphabet.replaceAll("\\s+", "");
this.seps = this.seps.replaceAll("\\s+", "");
this.seps = this.consistentShuffle(this.seps, this.salt);
double sepDiv = 3.5;
if ((this.seps.equals(""))
|| ((this.alphabet.length() / this.seps.length()) > sepDiv)) {
int seps_len = (int) Math.ceil(this.alphabet.length() / sepDiv);
if (seps_len == 1) {
seps_len++;
}
if (seps_len > this.seps.length()) {
int diff = seps_len - this.seps.length();
this.seps += this.alphabet.substring(0, diff);
this.alphabet = this.alphabet.substring(diff);
} else {
this.seps = this.seps.substring(0, seps_len);
}
}
this.alphabet = this.consistentShuffle(this.alphabet, this.salt);
// use double to round up
int guardDiv = 12;
int guardCount = (int) Math.ceil((double) this.alphabet.length()
/ guardDiv);
if (this.alphabet.length() < 3) {
this.guards = this.seps.substring(0, guardCount);
this.seps = this.seps.substring(guardCount);
} else {
this.guards = this.alphabet.substring(0, guardCount);
this.alphabet = this.alphabet.substring(guardCount);
}
}
/**
* Encrypt numbers to string
*
* @param numbers
* the numbers to encrypt
* @return the encrypt string
*/
public String encode(long... numbers) {
for (long number : numbers) {
if (number > 9007199254740992L) {
throw new IllegalArgumentException(
"number can not be greater than 9007199254740992L");
}
}
String retval = "";
if (numbers.length == 0) {
return retval;
}
return this._encode(numbers);
}
/**
* Decrypt string to numbers
*
* @param hash
* the encrypt string
* @return decryped numbers
*/
public long[] decode(String hash) {
long[] ret = {};
if (hash.equals(""))
return ret;
return this._decode(hash, this.alphabet);
}
/**
* Encrypt hexa to string
*
* @param hexa
* the hexa to encrypt
* @return the encrypt string
*/
public String encodeHex(String hexa) {
if (!hexa.matches("^[0-9a-fA-F]+$"))
return "";
List<Long> matched = new ArrayList<Long>();
Matcher matcher = Pattern.compile("[\\w\\W]{1,12}").matcher(hexa);
while (matcher.find())
matched.add(Long.parseLong("1" + matcher.group(), 16));
// conversion
long[] result = new long[matched.size()];
for (int i = 0; i < matched.size(); i++)
result[i] = matched.get(i);
return this._encode(result);
}
/**
* Decrypt string to numbers
*
* @param hash
* the encrypt string
* @return decryped numbers
*/
public String decodeHex(String hash) {
String result = "";
long[] numbers = this.decode(hash);
for (long number : numbers) {
result += Long.toHexString(number).substring(1);
}
return result;
}
private String _encode(long... numbers) {
int numberHashInt = 0;
for (int i = 0; i < numbers.length; i++) {
numberHashInt += (numbers[i] % (i + 100));
}
String alphabet = this.alphabet;
char ret = alphabet.toCharArray()[numberHashInt % alphabet.length()];
// char lottery = ret;
long num;
int sepsIndex, guardIndex;
String buffer, ret_str = ret + "";
char guard;
for (int i = 0; i < numbers.length; i++) {
num = numbers[i];
buffer = ret + this.salt + alphabet;
alphabet = this.consistentShuffle(alphabet,
buffer.substring(0, alphabet.length()));
String last = this.hash(num, alphabet);
ret_str += last;
if (i + 1 < numbers.length) {
num %= ((int) last.toCharArray()[0] + i);
sepsIndex = (int) (num % this.seps.length());
ret_str += this.seps.toCharArray()[sepsIndex];
}
}
if (ret_str.length() < this.minHashLength) {
guardIndex = (numberHashInt + (int) (ret_str.toCharArray()[0]))
% this.guards.length();
guard = this.guards.toCharArray()[guardIndex];
ret_str = guard + ret_str;
if (ret_str.length() < this.minHashLength) {
guardIndex = (numberHashInt + (int) (ret_str.toCharArray()[2]))
% this.guards.length();
guard = this.guards.toCharArray()[guardIndex];
ret_str += guard;
}
}
int halfLen = alphabet.length() / 2;
while (ret_str.length() < this.minHashLength) {
alphabet = this.consistentShuffle(alphabet, alphabet);
ret_str = alphabet.substring(halfLen) + ret_str
+ alphabet.substring(0, halfLen);
int excess = ret_str.length() - this.minHashLength;
if (excess > 0) {
int start_pos = excess / 2;
ret_str = ret_str.substring(start_pos, start_pos
+ this.minHashLength);
}
}
return ret_str;
}
private long[] _decode(String hash, String alphabet) {
ArrayList<Long> ret = new ArrayList<Long>();
int i = 0;
String regexp = "[" + this.guards + "]";
String hashBreakdown = hash.replaceAll(regexp, " ");
String[] hashArray = hashBreakdown.split(" ");
if (hashArray.length == 3 || hashArray.length == 2) {
i = 1;
}
hashBreakdown = hashArray[i];
char lottery = hashBreakdown.toCharArray()[0];
hashBreakdown = hashBreakdown.substring(1);
hashBreakdown = hashBreakdown.replaceAll("[" + this.seps + "]", " ");
hashArray = hashBreakdown.split(" ");
String subHash, buffer;
for (String aHashArray : hashArray) {
subHash = aHashArray;
buffer = lottery + this.salt + alphabet;
alphabet = this.consistentShuffle(alphabet,
buffer.substring(0, alphabet.length()));
ret.add(this.unhash(subHash, alphabet));
}
// transform from List<Long> to long[]
long[] arr = new long[ret.size()];
for (int k = 0; k < arr.length; k++) {
arr[k] = ret.get(k);
}
if (!this._encode(arr).equals(hash)) {
arr = new long[0];
}
return arr;
}
/* Private methods */
private String consistentShuffle(String alphabet, String salt) {
if (salt.length() <= 0)
return alphabet;
char[] arr = salt.toCharArray();
int asc_val, j;
char tmp;
for (int i = alphabet.length() - 1, v = 0, p = 0; i > 0; i--, v++) {
v %= salt.length();
asc_val = (int) arr[v];
p += asc_val;
j = (asc_val + v + p) % i;
tmp = alphabet.charAt(j);
alphabet = alphabet.substring(0, j) + alphabet.charAt(i)
+ alphabet.substring(j + 1);
alphabet = alphabet.substring(0, i) + tmp
+ alphabet.substring(i + 1);
}
return alphabet;
}
private String hash(long input, String alphabet) {
String hash = "";
int alphabetLen = alphabet.length();
char[] arr = alphabet.toCharArray();
do {
hash = arr[(int) (input % alphabetLen)] + hash;
input /= alphabetLen;
} while (input > 0);
return hash;
}
private Long unhash(String input, String alphabet) {
long number = 0, pos;
char[] input_arr = input.toCharArray();
for (int i = 0; i < input.length(); i++) {
pos = alphabet.indexOf(input_arr[i]);
number += pos * Math.pow(alphabet.length(), input.length() - i - 1);
}
return number;
}
public static int checkedCast(long value) {
int result = (int) value;
if (result != value) {
// don't use checkArgument here, to avoid boxing
throw new IllegalArgumentException("Out of range: " + value);
}
return result;
}
/**
* Get version
*
* @return version
*/
public String getVersion() {
return "1.0.0";
}
}