All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.javersion.util.BinaryEncoder Maven / Gradle / Ivy

There is a newer version: 0.15.3
Show newest version
package org.javersion.util;

import static java.lang.String.format;
import static java.util.Arrays.copyOf;
import static java.util.Arrays.fill;

import javax.annotation.concurrent.Immutable;

/**
 * Configurable binary encoder:
 * 
    *
  • Configurable charset of length n^2.
  • *
  • Configurable aliases (e.g. 1 l i for I).
  • *
  • Configurable direction: leftover zeros first as in binary format of numbers or at the end as in base encoders. * Encoding numbers in reverse retains the numeric order.
  • *
  • Configurable signed/unsigned encoding for numeric encoders.
  • *
  • Fluent builder for defining encoders.
  • *
* Reverse encoding is especially useful for text-based indexing. * * DISCLAIMER: *
    *
  • Flexibility comes with a price: any specialized encoder will be faster (e.g. * Java's Base64 is roughly twice as fast).
  • *
  • Numbers are encoded by bytes, so the length of the result is constant for the type of number.
  • *
  • Splitting result into lines of some max length is not supported.
  • *
  • Padding is not supported.
  • *
*/ @Immutable public abstract class BinaryEncoder { public static final BinaryEncoder HEX; public static final BinaryEncoder HEX_ALIASED; /** * No-padding Base32 encoder. */ public static final BinaryEncoder BASE32; /** * Douglas Crockford's Base32 alternative. Result is comparable as * alphabet is in lexical order. */ public static final BinaryEncoder BASE32_CROCKFORD; /** * Number encoder using Crockford's alphabet. */ public static final BinaryEncoder BASE32_CROCKFORD_NUMBER; /** * No-padding Base64 encoder. */ public static final BinaryEncoder BASE64; public static final BinaryEncoder BASE64_URL; public static final BinaryEncoder NUMBER_BASE64_URL; static { Builder builder; builder = new Builder("0123456789ABCDEF") .withAliases(" abcdef"); HEX = builder.buildUnsignedNumberEncoder(); builder.withAliasesFor('0', "oO") .withAliasesFor('1', "iIl"); HEX_ALIASED = builder.buildUnsignedNumberEncoder(); builder = new Builder("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567") .withAliases("abcdefghijklmnopqrstuvwxyz"); BASE32 = builder.buildBaseEncoder(); builder = new Builder("0123456789ABCDEFGHJKMNPQRSTVWXYZ") .withAliases(" abcdefghjkmnpqrstvwxyz") .withAliasesFor('0', "oO") .withAliasesFor('1', "iIlL"); BASE32_CROCKFORD = builder.buildBaseEncoder(); BASE32_CROCKFORD_NUMBER = builder.buildUnsignedNumberEncoder(); builder = new Builder("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); BASE64 = builder.buildBaseEncoder(); builder = new Builder("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"); BASE64_URL = builder.buildBaseEncoder(); builder = new Builder("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"); NUMBER_BASE64_URL = builder.buildUnsignedNumberEncoder(); } public static final class Builder { private int maxChar = -1; private final char[] numberToChar; private int[] charToNumber; public Builder(String chars) { this(chars.toCharArray()); } public Builder(char... chars) { numberToChar = copyOf(chars, chars.length); setMaxChar(chars); charToNumber = new int[maxChar + 1]; fill(charToNumber, -1); for (int i=0; i < chars.length; i++) { char ch = chars[i]; verify(ch); numberToChar[i] = ch; charToNumber[ch] = i; } } public Builder withAliasesFor(char ch, String aliases) { return withAliasesFor(ch, aliases.toCharArray()); } public Builder withAliasesFor(char ch, char... aliases) { setMaxChar(aliases); ensureCharToNumberSize(); for (char alias : aliases) { verify(alias); charToNumber[alias] = alias; } return this; } private void ensureCharToNumberSize() { int oldSize = charToNumber.length; if (oldSize <= maxChar) { charToNumber = copyOf(charToNumber, maxChar + 1); } fill(charToNumber, oldSize, charToNumber.length, -1); } public Builder withAliases(String aliases) { Check.that(aliases.length() <= numberToChar.length, "Expected positional aliases length to be same or less as main chars. Use space to skip."); char[] chars = aliases.toCharArray(); setMaxChar(chars); ensureCharToNumberSize(); for (int i=0; i < chars.length; i++) { char alias = chars[i]; if (alias != ' ') { verify(alias); charToNumber[alias] = i; } } return this; } public NumberEncoder buildUnsignedNumberEncoder() { return new NumberEncoder(numberToChar, charToNumber); } public NumberEncoder buildSignedNumberEncoder() { return new SignedNumberEncoder(numberToChar, charToNumber); } public BaseEncoder buildBaseEncoder() { return new BaseEncoder(numberToChar, charToNumber); } private void verify(char ch) { Check.that(charToNumber[ch] == -1, "Duplicate mapping for %s", ch); } private void setMaxChar(char[] chars) { for (int i=0; i < chars.length; i++) { int ch = chars[i]; if (maxChar < ch) { maxChar = ch; } } } } protected final int encodingBitLen; final byte mask; final char[] numberToChar; final int[] charToNumber; private BinaryEncoder(char[] numberToChar, int[] charToNumber) { Check.notNull(numberToChar, "toChar"); Check.notNull(charToNumber, "charToNumber"); this.numberToChar = copyOf(numberToChar, numberToChar.length); this.charToNumber = copyOf(charToNumber, charToNumber.length); int radix = numberToChar.length; Check.that(Integer.bitCount(radix) == 1, "radix should be ^2"); Check.that(radix >= 2, "radix should be > 2"); Check.that(radix <= 256, "radix should be <= 256"); this.encodingBitLen = Integer.bitCount(radix-1); this.mask = (byte) (radix - 1); } public String encode(byte[] bytes) { return encode(new Bytes.Array(bytes)); } public byte[] decode(String str) { return decode(str, new Bytes.Array(str.length() * encodingBitLen / 8)).getBytes(); } public String encodeLong(long l) { return encode(new Bytes.Long(l)); } public String encodeInt(int i) { return encode(new Bytes.Integer(i)); } public long decodeLong(String str) { return decode(str, new Bytes.Long(0)).getLong(); } public int decodeInt(String str) { return decode(str, new Bytes.Integer(0)).getInt(); } abstract String encode(Bytes bytes); abstract T decode(String str, T bytes); void throwIllegalCharacterException(String str, int index) { throw new IllegalArgumentException(format("Illegal character %s at %s", str.charAt(index), index)); } int charLen(int byteLen) { // ceil return 1 + ((byteLen * 8 - 1) / encodingBitLen); } public static class NumberEncoder extends BinaryEncoder { private NumberEncoder(char[] numberToChar, int[] charToNumber) { super(numberToChar, charToNumber); for (int i = 1; i < numberToChar.length; i++) { Check.that(numberToChar[i-1] < numberToChar[i], "Expected alphabet to be in lexical order! Got %s before %s", numberToChar[i-1], numberToChar[i]); } } @Override String encode(Bytes bytes) { int charLen = charLen(bytes.length()); char[] chars = new char[charLen]; for (int bitIndex = (bytes.length() * 8) - encodingBitLen, charIndex = charLen - 1; charIndex >= 0; bitIndex -= encodingBitLen, charIndex--) { int num = bytes.getNumber(bitIndex, encodingBitLen); chars[charIndex] = numberToChar[num]; } return new String(chars); } @Override T decode(String str, T bytes) { int charLen = str.length(); for (int bitIndex = (bytes.length() * 8) - encodingBitLen, charIndex = charLen - 1; charIndex >= 0; bitIndex -= encodingBitLen, charIndex--) { int charToNumberIndex = str.charAt(charIndex); if (charToNumberIndex >= charToNumber.length) { throwIllegalCharacterException(str, charIndex); } int number = charToNumber[charToNumberIndex]; if (number < 0) { throwIllegalCharacterException(str, charIndex); } bytes.setNumber(number, bitIndex, encodingBitLen); } return bytes; } } public static class SignedNumberEncoder extends NumberEncoder { private SignedNumberEncoder(char[] numberToChar, int[] charToNumber) { super(numberToChar, charToNumber); } @Override public String encodeLong(long l) { return super.encodeLong(l - Long.MIN_VALUE); } @Override public String encodeInt(int i) { return super.encodeInt(i - Integer.MIN_VALUE); } @Override public long decodeLong(String str) { return super.decodeLong(str) + Long.MIN_VALUE; } @Override public int decodeInt(String str) { return super.decodeInt(str) + Integer.MIN_VALUE; } } public static class BaseEncoder extends BinaryEncoder { private BaseEncoder(char[] numberToChar, int[] charToNumber) { super(numberToChar, charToNumber); } @Override String encode(Bytes bytes) { int charLen = charLen(bytes.length()); char[] chars = new char[charLen]; for (int bitIndex = 0, charIndex = 0; charIndex < charLen; bitIndex += encodingBitLen, charIndex++) { int num = bytes.getNumber(bitIndex, encodingBitLen); chars[charIndex] = numberToChar[num]; } return new String(chars); } @Override T decode(String str, T bytes) { int charLen = str.length(); for (int bitIndex = 0, charIndex = 0; charIndex < charLen; bitIndex += encodingBitLen, charIndex++) { int charToNumberIndex = str.charAt(charIndex); if (charToNumberIndex >= charToNumber.length) { throwIllegalCharacterException(str, charIndex); } int number = charToNumber[charToNumberIndex]; if (number < 0) { throwIllegalCharacterException(str, charIndex); } bytes.setNumber(number, bitIndex, encodingBitLen); } return bytes; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy