/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.Queue;
import java.util.regex.Pattern;
import org.ldaptive.io.Hex;
/**
* Provides utility methods for this package.
*
* @author Middleware Services
*/
public final class LdapUtils
{
/** Size of buffer in bytes to use when reading files. */
private static final int READ_BUFFER_SIZE = 128;
/** Prime number to assist in calculating hash codes. */
private static final int HASH_CODE_PRIME = 113;
/** Pattern to match ipv4 addresses. */
private static final Pattern IPV4_PATTERN = Pattern.compile(
"^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)" +
"(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$");
/** Pattern to match ipv6 addresses. */
private static final Pattern IPV6_STD_PATTERN = Pattern.compile("^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$");
/** Pattern to match ipv6 hex compressed addresses. */
private static final Pattern IPV6_HEX_COMPRESSED_PATTERN = Pattern.compile(
"^((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::" +
"((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)$");
/** Prefix used to indicate a classpath resource. */
private static final String CLASSPATH_PREFIX = "classpath:";
/** Prefix used to indicate a file resource. */
private static final String FILE_PREFIX = "file:";
/** Default constructor. */
private LdapUtils() {}
/**
* This will convert the supplied value to a base64 encoded string. Returns null if the supplied byte array is null.
*
* @param value to base64 encode
*
* @return base64 encoded value
*/
public static String base64Encode(final byte[] value)
{
return value != null ? new String(Base64.getEncoder().encode(value), StandardCharsets.UTF_8) : null;
}
/**
* This will convert the supplied value to a base64 encoded string. Returns null if the supplied string is null.
*
* @param value to base64 encode
*
* @return base64 encoded value
*/
public static String base64Encode(final String value)
{
return value != null ? base64Encode(value.getBytes(StandardCharsets.UTF_8)) : null;
}
/**
* This will convert the supplied value to a UTF-8 encoded string. Returns null if the supplied byte array is null.
*
* @param value to UTF-8 encode
*
* @return UTF-8 encoded value
*/
public static String utf8Encode(final byte[] value)
{
return value != null ? new String(value, StandardCharsets.UTF_8) : null;
}
/**
* This will convert the supplied value to a UTF-8 encoded byte array. Returns null if the supplied string is null.
*
* @param value to UTF-8 encode
*
* @return UTF-8 encoded value
*/
public static byte[] utf8Encode(final String value)
{
return value != null ? value.getBytes(StandardCharsets.UTF_8) : null;
}
/**
* This will convert the supplied value to a hex encoded string. Returns null if the supplied byte array is null.
*
* @param value to hex encode
*
* @return hex encoded value
*/
public static char[] hexEncode(final byte[] value)
{
return value != null ? Hex.encode(value) : null;
}
/**
* This will convert the supplied value to a hex encoded string. Returns null if the supplied char array is null.
*
* @param value to hex encode
*
* @return hex encoded value
*/
public static char[] hexEncode(final char... value)
{
return value != null ? hexEncode(utf8Encode(String.valueOf(value))) : null;
}
/**
* Implementation of percent encoding as described in RFC 3986 section 2.1.
*
* @param value to encode
*
* @return percent encoded value
*/
public static String percentEncode(final String value)
{
if (value == null) {
return null;
}
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < value.length(); i++) {
final char ch = value.charAt(i);
// uppercase
if (ch >= 'A' && ch <= 'Z') {
sb.append(ch);
// lowercase
} else if (ch >= 'a' && ch <= 'z') {
sb.append(ch);
// digit
} else if (ch >= '0' && ch <= '9') {
sb.append(ch);
} else {
// unreserved and reserved
switch (ch) {
case '-':
case '.':
case '_':
case '~':
case '!':
case '$':
case '&':
case '\'':
case '(':
case ')':
case '*':
case '+':
case ',':
case ';':
case '=':
sb.append(ch);
break;
default:
sb.append("%");
// CheckStyle:MagicNumber OFF
if (ch <= 0x7F) {
sb.append(hexEncode(new byte[] {(byte) (ch & 0x7F)}));
} else {
sb.append(hexEncode(utf8Encode(String.valueOf(ch))));
}
// CheckStyle:MagicNumber ON
}
}
}
return sb.toString();
}
/**
* This will decode the supplied value as a base64 encoded string to a byte[]. Returns null if the supplied string is
* null.
*
* @param value to base64 decode
*
* @return base64 decoded value
*/
public static byte[] base64Decode(final String value)
{
return value != null ? Base64.getDecoder().decode(value.getBytes()) : null;
}
/**
* This will decode the supplied value as a hex encoded string to a byte[]. Returns null if the supplied character
* array is null.
*
* @param value to hex decode
*
* @return hex decoded value
*/
public static byte[] hexDecode(final char[] value)
{
return value != null ? Hex.decode(value) : null;
}
/**
* Implementation of percent decoding as described in RFC 3986 section 2.1.
*
* @param value to decode
*
* @return percent decoded value
*/
public static String percentDecode(final String value)
{
if (value == null || !value.contains("%")) {
return value;
}
final StringBuilder sb = new StringBuilder();
int pos = 0;
while (pos < value.length()) {
final char c = value.charAt(pos++);
if (c == '%') {
final char[] hex = new char[] {
value.charAt(pos++),
value.charAt(pos++),
};
sb.append(utf8Encode(hexDecode(hex)));
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* Reads the data at the supplied URL and returns it as a byte array.
*
* @param url to read
*
* @return bytes read from the URL
*
* @throws IOException if an error occurs reading data
*/
public static byte[] readURL(final URL url)
throws IOException
{
return readInputStream(url.openStream());
}
/**
* Reads the data in the supplied stream and returns it as a byte array.
*
* @param is stream to read
*
* @return bytes read from the stream
*
* @throws IOException if an error occurs reading data
*/
public static byte[] readInputStream(final InputStream is)
throws IOException
{
final ByteArrayOutputStream data = new ByteArrayOutputStream();
try {
final byte[] buffer = new byte[READ_BUFFER_SIZE];
int length;
while ((length = is.read(buffer)) != -1) {
data.write(buffer, 0, length);
}
} finally {
is.close();
data.close();
}
return data.toByteArray();
}
/**
* Concatenates multiple arrays together.
*
* @param <T> type of array
* @param first array to concatenate. Cannot be null.
* @param rest of the arrays to concatenate. May be null.
*
* @return array containing the concatenation of all parameters
*/
@SuppressWarnings("unchecked")
public static <T> T[] concatArrays(final T[] first, final T[]... rest)
{
int totalLength = first.length;
for (T[] array : rest) {
if (array != null) {
totalLength += array.length;
}
}
final T[] result = Arrays.copyOf(first, totalLength);
int offset = first.length;
for (T[] array : rest) {
if (array != null) {
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
}
return result;
}
/**
* Determines equality of the supplied objects. Array types are automatically detected.
*
* @param o1 to test equality of
* @param o2 to test equality of
*
* @return whether o1 equals o2
*/
public static boolean areEqual(final Object o1, final Object o2)
{
if (o1 == o2) {
return true;
}
boolean areEqual;
if (o1 instanceof boolean[] && o2 instanceof boolean[]) {
areEqual = Arrays.equals((boolean[]) o1, (boolean[]) o2);
} else if (o1 instanceof byte[] && o2 instanceof byte[]) {
areEqual = Arrays.equals((byte[]) o1, (byte[]) o2);
} else if (o1 instanceof char[] && o2 instanceof char[]) {
areEqual = Arrays.equals((char[]) o1, (char[]) o2);
} else if (o1 instanceof double[] && o2 instanceof double[]) {
areEqual = Arrays.equals((double[]) o1, (double[]) o2);
} else if (o1 instanceof float[] && o2 instanceof float[]) {
areEqual = Arrays.equals((float[]) o1, (float[]) o2);
} else if (o1 instanceof int[] && o2 instanceof int[]) {
areEqual = Arrays.equals((int[]) o1, (int[]) o2);
} else if (o1 instanceof long[] && o2 instanceof long[]) {
areEqual = Arrays.equals((long[]) o1, (long[]) o2);
} else if (o1 instanceof short[] && o2 instanceof short[]) {
areEqual = Arrays.equals((short[]) o1, (short[]) o2);
} else if (o1 instanceof Object[] && o2 instanceof Object[]) {
areEqual = Arrays.deepEquals((Object[]) o1, (Object[]) o2);
} else {
areEqual = o1 != null && o1.equals(o2);
}
return areEqual;
}
/**
* Computes a hash code for the supplied objects using the supplied seed. If a Collection type is found it is iterated
* over.
*
* @param seed odd/prime number
* @param objects to calculate hashCode for
*
* @return hash code for the supplied objects
*/
public static int computeHashCode(final int seed, final Object... objects)
{
if (objects == null || objects.length == 0) {
return seed * HASH_CODE_PRIME;
}
int hc = seed;
for (Object object : objects) {
hc = HASH_CODE_PRIME * hc;
if (object != null) {
if (object instanceof List<?> || object instanceof Queue<?>) {
int index = 1;
for (Object o : (Collection<?>) object) {
hc += computeHashCode(o) * index++;
}
} else if (object instanceof Collection<?>) {
for (Object o : (Collection<?>) object) {
hc += computeHashCode(o);
}
} else {
hc += computeHashCode(object);
}
}
}
return hc;
}
/**
* Computes a hash code for the supplied object. Checks for arrays of primitives and Objects then delegates to the
* {@link Arrays} class. Otherwise {@link Object#hashCode()} is invoked.
*
* @param object to calculate hash code for
*
* @return hash code
*/
private static int computeHashCode(final Object object)
{
int hc = 0;
if (object instanceof boolean[]) {
hc += Arrays.hashCode((boolean[]) object);
} else if (object instanceof byte[]) {
hc += Arrays.hashCode((byte[]) object);
} else if (object instanceof char[]) {
hc += Arrays.hashCode((char[]) object);
} else if (object instanceof double[]) {
hc += Arrays.hashCode((double[]) object);
} else if (object instanceof float[]) {
hc += Arrays.hashCode((float[]) object);
} else if (object instanceof int[]) {
hc += Arrays.hashCode((int[]) object);
} else if (object instanceof long[]) {
hc += Arrays.hashCode((long[]) object);
} else if (object instanceof short[]) {
hc += Arrays.hashCode((short[]) object);
} else if (object instanceof Object[]) {
hc += Arrays.hashCode((Object[]) object);
} else {
hc += object.hashCode();
}
return hc;
}
/**
* Returns whether the supplied string represents an IP address. Matches both IPv4 and IPv6 addresses.
*
* @param s to match
*
* @return whether the supplied string represents an IP address
*/
public static boolean isIPAddress(final String s)
{
return
s != null &&
(IPV4_PATTERN.matcher(s).matches() || IPV6_STD_PATTERN.matcher(s).matches() ||
IPV6_HEX_COMPRESSED_PATTERN.matcher(s).matches());
}
/**
* Returns whether the supplied string starts with {@link #CLASSPATH_PREFIX} or {@link #FILE_PREFIX}.
*
* @param s to inspect
*
* @return whether the supplied string represents a resource
*/
public static boolean isResource(final String s)
{
return s != null && (s.startsWith(CLASSPATH_PREFIX) || s.startsWith(FILE_PREFIX));
}
/**
* Parses the supplied path and returns an input stream based on the prefix in the path. If a path is prefixed with
* the string "classpath:" it is interpreted as a classpath specification. If a path is prefixed with the string
* "file:" it is interpreted as a file path.
*
* @param path that designates a resource
*
* @return input stream to read the resource
*
* @throws IOException if the resource cannot be read
* @throws IllegalArgumentException if path is not prefixed with either 'classpath:' or 'file:'
*/
public static InputStream getResource(final String path)
throws IOException
{
InputStream is;
if (path.startsWith(CLASSPATH_PREFIX)) {
is = LdapUtils.class.getResourceAsStream(path.substring(CLASSPATH_PREFIX.length()));
} else if (path.startsWith(FILE_PREFIX)) {
is = new FileInputStream(new File(path.substring(FILE_PREFIX.length())));
} else {
throw new IllegalArgumentException(
"path '" + path + "' must start with either " + CLASSPATH_PREFIX + " or " + FILE_PREFIX);
}
return is;
}
}