com.unboundid.util.StaticUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of unboundid-ldapsdk Show documentation
Show all versions of unboundid-ldapsdk Show documentation
The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use
Java API for communicating with LDAP directory servers and performing
related tasks like reading and writing LDIF, encoding and decoding data
using base64 and ASN.1 BER, and performing secure communication. This
package contains the Standard Edition of the LDAP SDK, which is a
complete, general-purpose library for communicating with LDAPv3 directory
servers.
/*
* Copyright 2007-2022 Ping Identity Corporation
* All Rights Reserved.
*/
/*
* Copyright 2007-2022 Ping Identity Corporation
*
* Licensed 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.
*/
/*
* Copyright (C) 2007-2022 Ping Identity Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (GPLv2 only)
* or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*/
package com.unboundid.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.lang.reflect.Array;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.UUID;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.NameResolver;
import com.unboundid.ldap.sdk.Version;
import static com.unboundid.util.UtilityMessages.*;
/**
* This class provides a number of static utility functions.
*/
@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
public final class StaticUtils
{
/**
* A pre-allocated byte array containing zero bytes.
*/
@NotNull public static final byte[] NO_BYTES = new byte[0];
/**
* A pre-allocated empty character array.
*/
@NotNull public static final char[] NO_CHARS = new char[0];
/**
* A pre-allocated empty control array.
*/
@NotNull public static final Control[] NO_CONTROLS = new Control[0];
/**
* A pre-allocated empty integer array.
*/
@NotNull public static final int[] NO_INTS = new int[0];
/**
* A pre-allocated empty string array.
*/
@NotNull public static final String[] NO_STRINGS = new String[0];
/**
* The end-of-line marker for the platform on which the LDAP SDK is
* currently running.
*/
@NotNull public static final String EOL =
getSystemProperty("line.separator", "\n");
/**
* The end-of-line marker that consists of a carriage return character
* followed by a line feed character, as used on Windows systems.
*/
@NotNull public static final String EOL_CR_LF = "\r\n";
/**
* The end-of-line marker that consists of just the line feed character, as
* used on UNIX-based systems.
*/
@NotNull public static final String EOL_LF = "\n";
/**
* A byte array containing the end-of-line marker for the platform on which
* the LDAP SDK is currently running.
*/
@NotNull public static final byte[] EOL_BYTES = getBytes(EOL);
/**
* A byte array containing the end-of-line marker that consists of a carriage
* return character followed by a line feed character, as used on Windows
* systems.
*/
@NotNull public static final byte[] EOL_BYTES_CR_LF = getBytes(EOL_CR_LF);
/**
* A byte array containing the end-of-line marker that consists of just the
* line feed character, as used on UNIX-based systems.
*/
@NotNull public static final byte[] EOL_BYTES_LF = getBytes(EOL_LF);
/**
* Indicates whether the unit tests are currently running.
*/
private static final boolean IS_WITHIN_UNIT_TESTS =
Boolean.getBoolean("com.unboundid.ldap.sdk.RunningUnitTests") ||
Boolean.getBoolean("com.unboundid.directory.server.RunningUnitTests");
/**
* The thread-local date formatter used to encode generalized time values.
*/
@NotNull private static final ThreadLocal
GENERALIZED_TIME_FORMATTERS = new ThreadLocal<>();
/**
* The thread-local date formatter used to encode RFC 3339 time values.
*/
@NotNull private static final ThreadLocal
RFC_3339_TIME_FORMATTERS = new ThreadLocal<>();
/**
* The {@code TimeZone} object that represents the UTC (universal coordinated
* time) time zone.
*/
@NotNull private static final TimeZone UTC_TIME_ZONE =
TimeZone.getTimeZone("UTC");
/**
* A set containing the names of attributes that will be considered sensitive
* by the {@code toCode} methods of various request and data structure types.
*/
@NotNull private static volatile Set
TO_CODE_SENSITIVE_ATTRIBUTE_NAMES = setOf("userpassword", "2.5.4.35",
"authpassword", "1.3.6.1.4.1.4203.1.3.4");
/**
* The width of the terminal window, in columns.
*/
public static final int TERMINAL_WIDTH_COLUMNS;
static
{
// Try to dynamically determine the size of the terminal window using the
// COLUMNS environment variable.
int terminalWidth = 80;
final String columnsEnvVar = getEnvironmentVariable("COLUMNS");
if (columnsEnvVar != null)
{
try
{
terminalWidth = Integer.parseInt(columnsEnvVar);
}
catch (final Exception e)
{
Debug.debugException(e);
}
}
TERMINAL_WIDTH_COLUMNS = terminalWidth;
}
/**
* An array containing the set of lowercase ASCII letters.
*/
@NotNull private static final char[] LOWERCASE_LETTERS =
"abcdefghijklmnopqrstuvwxyz".toCharArray();
/**
* An array containing the set of ASCII numeric digits.
*/
@NotNull private static final char[] NUMERIC_DIGITS =
"0123456789".toCharArray();
/**
* An array containing the set of ASCII alphanumeric characters. It will
* include both uppercase and lowercase letters.
*/
@NotNull private static final char[] ALPHANUMERIC_CHARACTERS =
("abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789").toCharArray();
/**
* Prevent this class from being instantiated.
*/
private StaticUtils()
{
// No implementation is required.
}
/**
* Retrieves the set of currently defined system properties. If possible,
* this will simply return the result of a call to
* {@code System.getProperties}. However, the LDAP SDK is known to be used in
* environments where a security manager prevents setting system properties,
* and in that case, calls to {@code System.getProperties} will be rejected
* with a {@code SecurityException} because the returned structure is mutable
* and could be used to alter system property values. In such cases, a new
* empty {@code Properties} object will be created, and may optionally be
* populated with the values of a specific set of named properties.
*
* @param propertyNames An optional set of property names whose values (if
* defined) should be included in the
* {@code Properties} object that will be returned if a
* security manager prevents retrieving the full set of
* system properties. This may be {@code null} or
* empty if no specific properties should be retrieved.
*
* @return The value returned by a call to {@code System.getProperties} if
* possible, or a newly-created properties map (possibly including
* the values of a specified set of system properties) if it is not
* possible to get a mutable set of the system properties.
*/
@NotNull()
public static Properties getSystemProperties(
@Nullable final String... propertyNames)
{
try
{
final Properties properties = System.getProperties();
final String forceThrowPropertyName =
StaticUtils.class.getName() + ".forceGetSystemPropertiesToThrow";
// To ensure that we can get coverage for the code below in which there is
// a restrictive security manager in place, look for a system property
// that will cause us to throw an exception.
final Object forceThrowPropertyValue =
properties.getProperty(forceThrowPropertyName);
if (forceThrowPropertyValue != null)
{
throw new SecurityException(forceThrowPropertyName + '=' +
forceThrowPropertyValue);
}
return properties;
}
catch (final SecurityException e)
{
Debug.debugException(e);
}
// If we have gotten here, then we can assume that a security manager
// prevents us from accessing all system properties. Create a new proper
final Properties properties = new Properties();
if (propertyNames != null)
{
for (final String propertyName : propertyNames)
{
final Object propertyValue = System.getProperty(propertyName);
if (propertyValue != null)
{
properties.put(propertyName, propertyValue);
}
}
}
return properties;
}
/**
* Retrieves the value of the specified system property.
*
* @param name The name of the system property for which to retrieve the
* value.
*
* @return The value of the requested system property, or {@code null} if
* that variable was not set or its value could not be retrieved
* (for example, because a security manager prevents it).
*/
@Nullable()
public static String getSystemProperty(@NotNull final String name)
{
try
{
return System.getProperty(name);
}
catch (final Throwable t)
{
// It is possible that the call to System.getProperty could fail under
// some security managers. In that case, simply swallow the error and
// act as if that system property is not set.
Debug.debugException(t);
return null;
}
}
/**
* Retrieves the value of the specified system property.
*
* @param name The name of the system property for which to retrieve
* the value.
* @param defaultValue The default value to return if the specified
* system property is not set or could not be
* retrieved.
*
* @return The value of the requested system property, or the provided
* default value if that system property was not set or its value
* could not be retrieved (for example, because a security manager
* prevents it).
*/
@Nullable()
public static String getSystemProperty(@NotNull final String name,
@Nullable final String defaultValue)
{
try
{
return System.getProperty(name, defaultValue);
}
catch (final Throwable t)
{
// It is possible that the call to System.getProperty could fail under
// some security managers. In that case, simply swallow the error and
// act as if that system property is not set.
Debug.debugException(t);
return defaultValue;
}
}
/**
* Attempts to set the value of the specified system property. Note that this
* may not be permitted by some security managers, in which case the attempt
* will have no effect.
*
* @param name The name of the System property to set. It must not be
* {@code null}.
* @param value The value to use for the system property. If it is
* {@code null}, then the property will be cleared.
*
* @return The former value of the system property, or {@code null} if it
* did not have a value or if it could not be set (for example,
* because a security manager prevents it).
*/
@Nullable()
public static String setSystemProperty(@NotNull final String name,
@Nullable final String value)
{
try
{
if (value == null)
{
return System.clearProperty(name);
}
else
{
return System.setProperty(name, value);
}
}
catch (final Throwable t)
{
// It is possible that the call to System.setProperty or
// System.clearProperty could fail under some security managers. In that
// case, simply swallow the error and act as if that system property is
// not set.
Debug.debugException(t);
return null;
}
}
/**
* Attempts to clear the value of the specified system property. Note that
* this may not be permitted by some security managers, in which case the
* attempt will have no effect.
*
* @param name The name of the System property to clear. It must not be
* {@code null}.
*
* @return The former value of the system property, or {@code null} if it
* did not have a value or if it could not be set (for example,
* because a security manager prevents it).
*/
@Nullable()
public static String clearSystemProperty(@NotNull final String name)
{
try
{
return System.clearProperty(name);
}
catch (final Throwable t)
{
// It is possible that the call to System.clearProperty could fail under
// some security managers. In that case, simply swallow the error and
// act as if that system property is not set.
Debug.debugException(t);
return null;
}
}
/**
* Retrieves a map of all environment variables defined in the JVM's process.
*
* @return A map of all environment variables defined in the JVM's process,
* or an empty map if no environment variables are set or the actual
* set could not be retrieved (for example, because a security
* manager prevents it).
*/
@NotNull()
public static Map getEnvironmentVariables()
{
try
{
return System.getenv();
}
catch (final Throwable t)
{
// It is possible that the call to System.getenv could fail under some
// security managers. In that case, simply swallow the error and pretend
// that the environment variable is not set.
Debug.debugException(t);
return Collections.emptyMap();
}
}
/**
* Retrieves the value of the specified environment variable.
*
* @param name The name of the environment variable for which to retrieve
* the value.
*
* @return The value of the requested environment variable, or {@code null}
* if that variable was not set or its value could not be retrieved
* (for example, because a security manager prevents it).
*/
@Nullable()
public static String getEnvironmentVariable(@NotNull final String name)
{
try
{
return System.getenv(name);
}
catch (final Throwable t)
{
// It is possible that the call to System.getenv could fail under some
// security managers. In that case, simply swallow the error and pretend
// that the environment variable is not set.
Debug.debugException(t);
return null;
}
}
/**
* Retrieves the value of the specified environment variable.
*
* @param name The name of the environment variable for which to
* retrieve the value.
* @param defaultValue The default value to use if the specified environment
* variable is not set. It may be {@code null} if no
* default should be used.
*
* @return The value of the requested environment variable, or {@code null}
* if that variable was not set or its value could not be retrieved
* (for example, because a security manager prevents it) and there
* is no default value.
*/
@Nullable()
public static String getEnvironmentVariable(@NotNull final String name,
@Nullable final String defaultValue)
{
final String value = getEnvironmentVariable(name);
if (value == null)
{
return defaultValue;
}
else
{
return value;
}
}
/**
* Attempts to set the desired log level for the specified logger. Note that
* this may not be permitted by some security managers, in which case the
* attempt will have no effect.
*
* @param logger The logger whose level should be updated.
* @param logLevel The log level to set for the logger.
*/
public static void setLoggerLevel(@NotNull final Logger logger,
@NotNull final Level logLevel)
{
try
{
logger.setLevel(logLevel);
}
catch (final Throwable t)
{
Debug.debugException(t);
}
}
/**
* Attempts to set the desired log level for the specified log handler. Note
* that this may not be permitted by some security managers, in which case the
* attempt will have no effect.
*
* @param logHandler The log handler whose level should be updated.
* @param logLevel The log level to set for the log handler.
*/
public static void setLogHandlerLevel(@NotNull final Handler logHandler,
@NotNull final Level logLevel)
{
try
{
logHandler.setLevel(logLevel);
}
catch (final Throwable t)
{
Debug.debugException(t);
}
}
/**
* Retrieves a UTF-8 byte representation of the provided string.
*
* @param s The string for which to retrieve the UTF-8 byte representation.
*
* @return The UTF-8 byte representation for the provided string.
*/
@NotNull()
public static byte[] getBytes(@Nullable final String s)
{
final int length;
if ((s == null) || ((length = s.length()) == 0))
{
return NO_BYTES;
}
final byte[] b = new byte[length];
for (int i=0; i < length; i++)
{
final char c = s.charAt(i);
if (c <= 0x7F)
{
b[i] = (byte) (c & 0x7F);
}
else
{
return s.getBytes(StandardCharsets.UTF_8);
}
}
return b;
}
/**
* Retrieves a byte array containing the UTF-8 representation of the bytes
* that comprise the provided Unicode code point.
*
* @param codePoint The code point for which to retrieve the UTF-8 bytes.
*
* @return A byte array containing the UTF-8 representation of the bytes that
* comprise the provided Unicode code point.
*/
@NotNull()
public static byte[] getBytesForCodePoint(final int codePoint)
{
if (codePoint <= 0x7F)
{
return new byte[] { (byte) codePoint };
}
else
{
final String codePointString = new String(new int[] { codePoint }, 0, 1);
return codePointString.getBytes(StandardCharsets.UTF_8);
}
}
/**
* Indicates whether the contents of the provided byte array represent an
* ASCII string, which is also known in LDAP terminology as an IA5 string.
* An ASCII string is one that contains only bytes in which the most
* significant bit is zero.
*
* @param b The byte array for which to make the determination. It must
* not be {@code null}.
*
* @return {@code true} if the contents of the provided array represent an
* ASCII string, or {@code false} if not.
*/
public static boolean isASCIIString(@NotNull final byte[] b)
{
for (final byte by : b)
{
if ((by & 0x80) == 0x80)
{
return false;
}
}
return true;
}
/**
* Indicates whether the contents of the provided string represent an ASCII
* string, which is also known in LDAP terminology as an IA5 string. An ASCII
* string is one that contains only bytes in which the most significant bit is
* zero.
*
* @param s The string for which to make the determination. It must not be
* {@code null}.
*
* @return {@code true} if the contents of the provided string represent an
* ASCII string, or {@code false} if not.
*/
public static boolean isASCIIString(@NotNull final String s)
{
return isASCIIString(getBytes(s));
}
/**
* Indicates whether the provided character is a printable ASCII character, as
* per RFC 4517 section 3.2. The only printable characters are:
*
* - All uppercase and lowercase ASCII alphabetic letters
* - All ASCII numeric digits
* - The following additional ASCII characters: single quote, left
* parenthesis, right parenthesis, plus, comma, hyphen, period, equals,
* forward slash, colon, question mark, space.
*
*
* @param c The character for which to make the determination.
*
* @return {@code true} if the provided character is a printable ASCII
* character, or {@code false} if not.
*/
public static boolean isPrintable(final char c)
{
if (((c >= 'a') && (c <= 'z')) ||
((c >= 'A') && (c <= 'Z')) ||
((c >= '0') && (c <= '9')))
{
return true;
}
switch (c)
{
case '\'':
case '(':
case ')':
case '+':
case ',':
case '-':
case '.':
case '=':
case '/':
case ':':
case '?':
case ' ':
return true;
default:
return false;
}
}
/**
* Indicates whether the contents of the provided byte array represent a
* printable LDAP string, as per RFC 4517 section 3.2. The only characters
* allowed in a printable string are:
*
* - All uppercase and lowercase ASCII alphabetic letters
* - All ASCII numeric digits
* - The following additional ASCII characters: single quote, left
* parenthesis, right parenthesis, plus, comma, hyphen, period, equals,
* forward slash, colon, question mark, space.
*
* If the provided array contains anything other than the above characters
* (i.e., if the byte array contains any non-ASCII characters, or any ASCII
* control characters, or if it contains excluded ASCII characters like
* the exclamation point, double quote, octothorpe, dollar sign, etc.), then
* it will not be considered printable.
*
* @param b The byte array for which to make the determination. It must
* not be {@code null}.
*
* @return {@code true} if the contents of the provided byte array represent
* a printable LDAP string, or {@code false} if not.
*/
public static boolean isPrintableString(@NotNull final byte[] b)
{
for (final byte by : b)
{
if ((by & 0x80) == 0x80)
{
return false;
}
if (((by >= 'a') && (by <= 'z')) ||
((by >= 'A') && (by <= 'Z')) ||
((by >= '0') && (by <= '9')))
{
continue;
}
switch (by)
{
case '\'':
case '(':
case ')':
case '+':
case ',':
case '-':
case '.':
case '=':
case '/':
case ':':
case '?':
case ' ':
continue;
default:
return false;
}
}
return true;
}
/**
* Indicates whether the provided string represents a printable LDAP string,
* as per RFC 4517 section 3.2. The only characters allowed in a printable
* string are:
*
* - All uppercase and lowercase ASCII alphabetic letters
* - All ASCII numeric digits
* - The following additional ASCII characters: single quote, left
* parenthesis, right parenthesis, plus, comma, hyphen, period, equals,
* forward slash, colon, question mark, space.
*
* If the provided array contains anything other than the above characters
* (i.e., if the byte array contains any non-ASCII characters, or any ASCII
* control characters, or if it contains excluded ASCII characters like
* the exclamation point, double quote, octothorpe, dollar sign, etc.), then
* it will not be considered printable.
*
* @param s The string for which to make the determination. It must not be
* {@code null}.
*
* @return {@code true} if the provided string represents a printable LDAP
* string, or {@code false} if not.
*/
public static boolean isPrintableString(@NotNull final String s)
{
final int length = s.length();
for (int i=0; i < length; i++)
{
final char c = s.charAt(i);
if ((c & 0x80) == 0x80)
{
return false;
}
if (((c >= 'a') && (c <= 'z')) ||
((c >= 'A') && (c <= 'Z')) ||
((c >= '0') && (c <= '9')))
{
continue;
}
switch (c)
{
case '\'':
case '(':
case ')':
case '+':
case ',':
case '-':
case '.':
case '=':
case '/':
case ':':
case '?':
case ' ':
continue;
default:
return false;
}
}
return true;
}
/**
* Indicates whether the specified Unicode code point represents a character
* that is believed to be displayable. Displayable characters include
* letters, numbers, spaces, dashes, punctuation, and symbols.
* Non-displayable characters include control characters, combining marks,
* enclosing marks, directionality indicators, format characters, and
* surrogate characters.
*
* @param codePoint The code point for which to make the determination.
*
* @return {@code true} if the specified Unicode character is believed to be
* displayable, or {@code false} if not.
*/
public static boolean isLikelyDisplayableCharacter(final int codePoint)
{
final int charType = Character.getType(codePoint);
switch (charType)
{
case Character.UPPERCASE_LETTER:
case Character.LOWERCASE_LETTER:
case Character.TITLECASE_LETTER:
case Character.MODIFIER_LETTER:
case Character.OTHER_LETTER:
case Character.DECIMAL_DIGIT_NUMBER:
case Character.LETTER_NUMBER:
case Character.OTHER_NUMBER:
case Character.SPACE_SEPARATOR:
case Character.DASH_PUNCTUATION:
case Character.START_PUNCTUATION:
case Character.END_PUNCTUATION:
case Character.CONNECTOR_PUNCTUATION:
case Character.OTHER_PUNCTUATION:
case Character.INITIAL_QUOTE_PUNCTUATION:
case Character.FINAL_QUOTE_PUNCTUATION:
case Character.MATH_SYMBOL:
case Character.CURRENCY_SYMBOL:
case Character.OTHER_SYMBOL:
return true;
default:
return false;
}
}
/**
*Indicates whether the provided string is comprised entirely of characters
* that are believed to be displayable (as determined by the
* {@link #isLikelyDisplayableCharacter} method).
*
* @param s The string for which to make the determination. It must not e
* {@code null}.
*
* @return {@code true} if the provided string is believed to be displayable,
* or {@code false} if not.
*/
public static boolean isLikelyDisplayableString(@NotNull final String s)
{
int pos = 0;
while (pos < s.length())
{
final int codePoint = s.codePointAt(pos);
if (! isLikelyDisplayableCharacter(codePoint))
{
return false;
}
pos += Character.charCount(codePoint);
}
return true;
}
/**
* Indicates whether the contents of the provided array are valid UTF-8.
*
* @param b The byte array to examine. It must not be {@code null}.
*
* @return {@code true} if the byte array can be parsed as a valid UTF-8
* string, or {@code false} if not.
*/
public static boolean isValidUTF8(@NotNull final byte[] b)
{
int i = 0;
while (i < b.length)
{
final byte currentByte = b[i++];
// If the most significant bit is not set, then this represents a valid
// single-byte character.
if ((currentByte & 0b1000_0000) == 0b0000_0000)
{
continue;
}
// If the first byte starts with 0b110, then it must be followed by
// another byte that starts with 0b10.
if ((currentByte & 0b1110_0000) == 0b1100_0000)
{
if (! hasExpectedSubsequentUTF8Bytes(b, i, 1))
{
return false;
}
i++;
continue;
}
// If the first byte starts with 0b1110, then it must be followed by two
// more bytes that start with 0b10.
if ((currentByte & 0b1111_0000) == 0b1110_0000)
{
if (! hasExpectedSubsequentUTF8Bytes(b, i, 2))
{
return false;
}
i += 2;
continue;
}
// If the first byte starts with 0b11110, then it must be followed by
// three more bytes that start with 0b10.
if ((currentByte & 0b1111_1000) == 0b1111_0000)
{
if (! hasExpectedSubsequentUTF8Bytes(b, i, 3))
{
return false;
}
i += 3;
continue;
}
// If the first byte starts with 0b111110, then it must be followed by
// four more bytes that start with 0b10.
if ((currentByte & 0b1111_1100) == 0b1111_1000)
{
if (! hasExpectedSubsequentUTF8Bytes(b, i, 4))
{
return false;
}
i += 4;
continue;
}
// If the first byte starts with 0b1111110, then it must be followed by
// five more bytes that start with 0b10.
if ((currentByte & 0b1111_1110) == 0b1111_1100)
{
if (! hasExpectedSubsequentUTF8Bytes(b, i, 5))
{
return false;
}
i += 5;
continue;
}
// This is not a valid first byte for a UTF-8 character.
return false;
}
// If we've gotten here, then the provided array represents a valid UTF-8
// string.
return true;
}
/**
* Ensures that the provided array has the expected number of bytes that start
* with 0b10 starting at the specified position in the array.
*
* @param b The byte array to examine.
* @param p The position in the byte array at which to start looking.
* @param n The number of bytes to examine.
*
* @return {@code true} if the provided byte array has the expected number of
* bytes that start with 0b10, or {@code false} if not.
*/
private static boolean hasExpectedSubsequentUTF8Bytes(@NotNull final byte[] b,
final int p,
final int n)
{
if (b.length < (p + n))
{
return false;
}
for (int i=0; i < n; i++)
{
if ((b[p+i] & 0b1100_0000) != 0b1000_0000)
{
return false;
}
}
return true;
}
/**
* Retrieves a string generated from the provided byte array using the UTF-8
* encoding.
*
* @param b The byte array for which to return the associated string.
*
* @return The string generated from the provided byte array using the UTF-8
* encoding.
*/
@NotNull()
public static String toUTF8String(@NotNull final byte[] b)
{
try
{
return new String(b, StandardCharsets.UTF_8);
}
catch (final Exception e)
{
// This should never happen.
Debug.debugException(e);
return new String(b);
}
}
/**
* Retrieves a string generated from the specified portion of the provided
* byte array using the UTF-8 encoding.
*
* @param b The byte array for which to return the associated string.
* @param offset The offset in the array at which the value begins.
* @param length The number of bytes in the value to convert to a string.
*
* @return The string generated from the specified portion of the provided
* byte array using the UTF-8 encoding.
*/
@NotNull()
public static String toUTF8String(@NotNull final byte[] b, final int offset,
final int length)
{
try
{
return new String(b, offset, length, StandardCharsets.UTF_8);
}
catch (final Exception e)
{
// This should never happen.
Debug.debugException(e);
return new String(b, offset, length);
}
}
/**
* Retrieves a version of the provided string with the first character
* converted to lowercase but all other characters retaining their original
* capitalization.
*
* @param s The string to be processed.
*
* @return A version of the provided string with the first character
* converted to lowercase but all other characters retaining their
* original capitalization. It may be {@code null} if the provided
* string is {@code null}.
*/
@Nullable()
public static String toInitialLowerCase(@Nullable final String s)
{
if ((s == null) || s.isEmpty())
{
return s;
}
else if (s.length() == 1)
{
return toLowerCase(s);
}
else
{
final char c = s.charAt(0);
if (((c >= 'A') && (c <= 'Z')) || (c < ' ') || (c > '~'))
{
final StringBuilder b = new StringBuilder(s);
b.setCharAt(0, Character.toLowerCase(c));
return b.toString();
}
else
{
return s;
}
}
}
/**
* Retrieves an all-lowercase version of the provided string.
*
* @param s The string for which to retrieve the lowercase version.
*
* @return An all-lowercase version of the provided string, or {@code null}
* if the provided string was {@code null}.
*/
@Nullable()
public static String toLowerCase(@Nullable final String s)
{
if (s == null)
{
return null;
}
final int length = s.length();
final char[] charArray = s.toCharArray();
for (int i=0; i < length; i++)
{
switch (charArray[i])
{
case 'A':
charArray[i] = 'a';
break;
case 'B':
charArray[i] = 'b';
break;
case 'C':
charArray[i] = 'c';
break;
case 'D':
charArray[i] = 'd';
break;
case 'E':
charArray[i] = 'e';
break;
case 'F':
charArray[i] = 'f';
break;
case 'G':
charArray[i] = 'g';
break;
case 'H':
charArray[i] = 'h';
break;
case 'I':
charArray[i] = 'i';
break;
case 'J':
charArray[i] = 'j';
break;
case 'K':
charArray[i] = 'k';
break;
case 'L':
charArray[i] = 'l';
break;
case 'M':
charArray[i] = 'm';
break;
case 'N':
charArray[i] = 'n';
break;
case 'O':
charArray[i] = 'o';
break;
case 'P':
charArray[i] = 'p';
break;
case 'Q':
charArray[i] = 'q';
break;
case 'R':
charArray[i] = 'r';
break;
case 'S':
charArray[i] = 's';
break;
case 'T':
charArray[i] = 't';
break;
case 'U':
charArray[i] = 'u';
break;
case 'V':
charArray[i] = 'v';
break;
case 'W':
charArray[i] = 'w';
break;
case 'X':
charArray[i] = 'x';
break;
case 'Y':
charArray[i] = 'y';
break;
case 'Z':
charArray[i] = 'z';
break;
default:
if (charArray[i] > 0x7F)
{
return s.toLowerCase();
}
break;
}
}
return new String(charArray);
}
/**
* Retrieves an all-uppercase version of the provided string.
*
* @param s The string for which to retrieve the uppercase version.
*
* @return An all-uppercase version of the provided string, or {@code null}
* if the provided string was {@code null}.
*/
@Nullable()
public static String toUpperCase(@Nullable final String s)
{
if (s == null)
{
return null;
}
final int length = s.length();
final char[] charArray = s.toCharArray();
for (int i=0; i < length; i++)
{
switch (charArray[i])
{
case 'a':
charArray[i] = 'A';
break;
case 'b':
charArray[i] = 'B';
break;
case 'c':
charArray[i] = 'C';
break;
case 'd':
charArray[i] = 'D';
break;
case 'e':
charArray[i] = 'E';
break;
case 'f':
charArray[i] = 'F';
break;
case 'g':
charArray[i] = 'G';
break;
case 'h':
charArray[i] = 'H';
break;
case 'i':
charArray[i] = 'I';
break;
case 'j':
charArray[i] = 'J';
break;
case 'k':
charArray[i] = 'K';
break;
case 'l':
charArray[i] = 'L';
break;
case 'm':
charArray[i] = 'M';
break;
case 'n':
charArray[i] = 'N';
break;
case 'o':
charArray[i] = 'O';
break;
case 'p':
charArray[i] = 'P';
break;
case 'q':
charArray[i] = 'Q';
break;
case 'r':
charArray[i] = 'R';
break;
case 's':
charArray[i] = 'S';
break;
case 't':
charArray[i] = 'T';
break;
case 'u':
charArray[i] = 'U';
break;
case 'v':
charArray[i] = 'V';
break;
case 'w':
charArray[i] = 'W';
break;
case 'x':
charArray[i] = 'X';
break;
case 'y':
charArray[i] = 'Y';
break;
case 'z':
charArray[i] = 'Z';
break;
default:
if (charArray[i] > 0x7F)
{
return s.toUpperCase();
}
break;
}
}
return new String(charArray);
}
/**
* Indicates whether the provided character is a valid hexadecimal digit.
*
* @param c The character for which to make the determination.
*
* @return {@code true} if the provided character does represent a valid
* hexadecimal digit, or {@code false} if not.
*/
public static boolean isHex(final char c)
{
switch (c)
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case 'a':
case 'A':
case 'b':
case 'B':
case 'c':
case 'C':
case 'd':
case 'D':
case 'e':
case 'E':
case 'f':
case 'F':
return true;
default:
return false;
}
}
/**
* Retrieves a hexadecimal representation of the provided byte.
*
* @param b The byte to encode as hexadecimal.
*
* @return A string containing the hexadecimal representation of the provided
* byte.
*/
@NotNull()
public static String toHex(final byte b)
{
final StringBuilder buffer = new StringBuilder(2);
toHex(b, buffer);
return buffer.toString();
}
/**
* Appends a hexadecimal representation of the provided byte to the given
* buffer.
*
* @param b The byte to encode as hexadecimal.
* @param buffer The buffer to which the hexadecimal representation is to be
* appended.
*/
public static void toHex(final byte b, @NotNull final StringBuilder buffer)
{
switch (b & 0xF0)
{
case 0x00:
buffer.append('0');
break;
case 0x10:
buffer.append('1');
break;
case 0x20:
buffer.append('2');
break;
case 0x30:
buffer.append('3');
break;
case 0x40:
buffer.append('4');
break;
case 0x50:
buffer.append('5');
break;
case 0x60:
buffer.append('6');
break;
case 0x70:
buffer.append('7');
break;
case 0x80:
buffer.append('8');
break;
case 0x90:
buffer.append('9');
break;
case 0xA0:
buffer.append('a');
break;
case 0xB0:
buffer.append('b');
break;
case 0xC0:
buffer.append('c');
break;
case 0xD0:
buffer.append('d');
break;
case 0xE0:
buffer.append('e');
break;
case 0xF0:
buffer.append('f');
break;
}
switch (b & 0x0F)
{
case 0x00:
buffer.append('0');
break;
case 0x01:
buffer.append('1');
break;
case 0x02:
buffer.append('2');
break;
case 0x03:
buffer.append('3');
break;
case 0x04:
buffer.append('4');
break;
case 0x05:
buffer.append('5');
break;
case 0x06:
buffer.append('6');
break;
case 0x07:
buffer.append('7');
break;
case 0x08:
buffer.append('8');
break;
case 0x09:
buffer.append('9');
break;
case 0x0A:
buffer.append('a');
break;
case 0x0B:
buffer.append('b');
break;
case 0x0C:
buffer.append('c');
break;
case 0x0D:
buffer.append('d');
break;
case 0x0E:
buffer.append('e');
break;
case 0x0F:
buffer.append('f');
break;
}
}
/**
* Appends a hexadecimal representation of the provided byte to the given
* buffer.
*
* @param b The byte to encode as hexadecimal.
* @param buffer The buffer to which the hexadecimal representation is to be
* appended.
*/
public static void toHex(final byte b, @NotNull final ByteStringBuffer buffer)
{
switch (b & 0xF0)
{
case 0x00:
buffer.append((byte) '0');
break;
case 0x10:
buffer.append((byte) '1');
break;
case 0x20:
buffer.append((byte) '2');
break;
case 0x30:
buffer.append((byte) '3');
break;
case 0x40:
buffer.append((byte) '4');
break;
case 0x50:
buffer.append((byte) '5');
break;
case 0x60:
buffer.append((byte) '6');
break;
case 0x70:
buffer.append((byte) '7');
break;
case 0x80:
buffer.append((byte) '8');
break;
case 0x90:
buffer.append((byte) '9');
break;
case 0xA0:
buffer.append((byte) 'a');
break;
case 0xB0:
buffer.append((byte) 'b');
break;
case 0xC0:
buffer.append((byte) 'c');
break;
case 0xD0:
buffer.append((byte) 'd');
break;
case 0xE0:
buffer.append((byte) 'e');
break;
case 0xF0:
buffer.append((byte) 'f');
break;
}
switch (b & 0x0F)
{
case 0x00:
buffer.append((byte) '0');
break;
case 0x01:
buffer.append((byte) '1');
break;
case 0x02:
buffer.append((byte) '2');
break;
case 0x03:
buffer.append((byte) '3');
break;
case 0x04:
buffer.append((byte) '4');
break;
case 0x05:
buffer.append((byte) '5');
break;
case 0x06:
buffer.append((byte) '6');
break;
case 0x07:
buffer.append((byte) '7');
break;
case 0x08:
buffer.append((byte) '8');
break;
case 0x09:
buffer.append((byte) '9');
break;
case 0x0A:
buffer.append((byte) 'a');
break;
case 0x0B:
buffer.append((byte) 'b');
break;
case 0x0C:
buffer.append((byte) 'c');
break;
case 0x0D:
buffer.append((byte) 'd');
break;
case 0x0E:
buffer.append((byte) 'e');
break;
case 0x0F:
buffer.append((byte) 'f');
break;
}
}
/**
* Retrieves a hexadecimal representation of the contents of the provided byte
* array. No delimiter character will be inserted between the hexadecimal
* digits for each byte.
*
* @param b The byte array to be represented as a hexadecimal string. It
* must not be {@code null}.
*
* @return A string containing a hexadecimal representation of the contents
* of the provided byte array.
*/
@NotNull()
public static String toHex(@NotNull final byte[] b)
{
Validator.ensureNotNull(b);
final StringBuilder buffer = new StringBuilder(2 * b.length);
toHex(b, buffer);
return buffer.toString();
}
/**
* Retrieves a hexadecimal representation of the contents of the provided byte
* array. No delimiter character will be inserted between the hexadecimal
* digits for each byte.
*
* @param b The byte array to be represented as a hexadecimal string.
* It must not be {@code null}.
* @param buffer A buffer to which the hexadecimal representation of the
* contents of the provided byte array should be appended.
*/
public static void toHex(@NotNull final byte[] b,
@NotNull final StringBuilder buffer)
{
toHex(b, null, buffer);
}
/**
* Retrieves a hexadecimal representation of the contents of the provided byte
* array. No delimiter character will be inserted between the hexadecimal
* digits for each byte.
*
* @param b The byte array to be represented as a hexadecimal
* string. It must not be {@code null}.
* @param delimiter A delimiter to be inserted between bytes. It may be
* {@code null} if no delimiter should be used.
* @param buffer A buffer to which the hexadecimal representation of the
* contents of the provided byte array should be appended.
*/
public static void toHex(@NotNull final byte[] b,
@Nullable final String delimiter,
@NotNull final StringBuilder buffer)
{
boolean first = true;
for (final byte bt : b)
{
if (first)
{
first = false;
}
else if (delimiter != null)
{
buffer.append(delimiter);
}
toHex(bt, buffer);
}
}
/**
* Retrieves a hex-encoded representation of the contents of the provided
* array, along with an ASCII representation of its contents next to it. The
* output will be split across multiple lines, with up to sixteen bytes per
* line. For each of those sixteen bytes, the two-digit hex representation
* will be appended followed by a space. Then, the ASCII representation of
* those sixteen bytes will follow that, with a space used in place of any
* byte that does not have an ASCII representation.
*
* @param array The array whose contents should be processed.
* @param indent The number of spaces to insert on each line prior to the
* first hex byte.
*
* @return A hex-encoded representation of the contents of the provided
* array, along with an ASCII representation of its contents next to
* it.
*/
@NotNull()
public static String toHexPlusASCII(@NotNull final byte[] array,
final int indent)
{
final StringBuilder buffer = new StringBuilder();
toHexPlusASCII(array, indent, buffer);
return buffer.toString();
}
/**
* Appends a hex-encoded representation of the contents of the provided array
* to the given buffer, along with an ASCII representation of its contents
* next to it. The output will be split across multiple lines, with up to
* sixteen bytes per line. For each of those sixteen bytes, the two-digit hex
* representation will be appended followed by a space. Then, the ASCII
* representation of those sixteen bytes will follow that, with a space used
* in place of any byte that does not have an ASCII representation.
*
* @param array The array whose contents should be processed.
* @param indent The number of spaces to insert on each line prior to the
* first hex byte.
* @param buffer The buffer to which the encoded data should be appended.
*/
public static void toHexPlusASCII(@Nullable final byte[] array,
final int indent,
@NotNull final StringBuilder buffer)
{
if ((array == null) || (array.length == 0))
{
return;
}
for (int i=0; i < indent; i++)
{
buffer.append(' ');
}
int pos = 0;
int startPos = 0;
while (pos < array.length)
{
toHex(array[pos++], buffer);
buffer.append(' ');
if ((pos % 16) == 0)
{
buffer.append(" ");
for (int i=startPos; i < pos; i++)
{
if ((array[i] < ' ') || (array[i] > '~'))
{
buffer.append(' ');
}
else
{
buffer.append((char) array[i]);
}
}
buffer.append(EOL);
startPos = pos;
if (pos < array.length)
{
for (int i=0; i < indent; i++)
{
buffer.append(' ');
}
}
}
}
// If the last line isn't complete yet, then finish it off.
if ((array.length % 16) != 0)
{
final int missingBytes = (16 - (array.length % 16));
for (int i=0; i < missingBytes; i++)
{
buffer.append(" ");
}
buffer.append(" ");
for (int i=startPos; i < array.length; i++)
{
if ((array[i] < ' ') || (array[i] > '~'))
{
buffer.append(' ');
}
else
{
buffer.append((char) array[i]);
}
}
buffer.append(EOL);
}
}
/**
* Retrieves the bytes that correspond to the provided hexadecimal string.
*
* @param hexString The hexadecimal string for which to retrieve the bytes.
* It must not be {@code null}, and there must not be any
* delimiter between bytes.
*
* @return The bytes that correspond to the provided hexadecimal string.
*
* @throws ParseException If the provided string does not represent valid
* hexadecimal data, or if the provided string does
* not contain an even number of characters.
*/
@NotNull()
public static byte[] fromHex(@NotNull final String hexString)
throws ParseException
{
if ((hexString.length() % 2) != 0)
{
throw new ParseException(
ERR_FROM_HEX_ODD_NUMBER_OF_CHARACTERS.get(hexString.length()),
hexString.length());
}
final byte[] decodedBytes = new byte[hexString.length() / 2];
for (int i=0, j=0; i < decodedBytes.length; i++, j+= 2)
{
switch (hexString.charAt(j))
{
case '0':
// No action is required.
break;
case '1':
decodedBytes[i] = 0x10;
break;
case '2':
decodedBytes[i] = 0x20;
break;
case '3':
decodedBytes[i] = 0x30;
break;
case '4':
decodedBytes[i] = 0x40;
break;
case '5':
decodedBytes[i] = 0x50;
break;
case '6':
decodedBytes[i] = 0x60;
break;
case '7':
decodedBytes[i] = 0x70;
break;
case '8':
decodedBytes[i] = (byte) 0x80;
break;
case '9':
decodedBytes[i] = (byte) 0x90;
break;
case 'a':
case 'A':
decodedBytes[i] = (byte) 0xA0;
break;
case 'b':
case 'B':
decodedBytes[i] = (byte) 0xB0;
break;
case 'c':
case 'C':
decodedBytes[i] = (byte) 0xC0;
break;
case 'd':
case 'D':
decodedBytes[i] = (byte) 0xD0;
break;
case 'e':
case 'E':
decodedBytes[i] = (byte) 0xE0;
break;
case 'f':
case 'F':
decodedBytes[i] = (byte) 0xF0;
break;
default:
throw new ParseException(ERR_FROM_HEX_NON_HEX_CHARACTER.get(j), j);
}
switch (hexString.charAt(j+1))
{
case '0':
// No action is required.
break;
case '1':
decodedBytes[i] |= 0x01;
break;
case '2':
decodedBytes[i] |= 0x02;
break;
case '3':
decodedBytes[i] |= 0x03;
break;
case '4':
decodedBytes[i] |= 0x04;
break;
case '5':
decodedBytes[i] |= 0x05;
break;
case '6':
decodedBytes[i] |= 0x06;
break;
case '7':
decodedBytes[i] |= 0x07;
break;
case '8':
decodedBytes[i] |= 0x08;
break;
case '9':
decodedBytes[i] |= 0x09;
break;
case 'a':
case 'A':
decodedBytes[i] |= 0x0A;
break;
case 'b':
case 'B':
decodedBytes[i] |= 0x0B;
break;
case 'c':
case 'C':
decodedBytes[i] |= 0x0C;
break;
case 'd':
case 'D':
decodedBytes[i] |= 0x0D;
break;
case 'e':
case 'E':
decodedBytes[i] |= 0x0E;
break;
case 'f':
case 'F':
decodedBytes[i] |= 0x0F;
break;
default:
throw new ParseException(ERR_FROM_HEX_NON_HEX_CHARACTER.get(j+1),
j+1);
}
}
return decodedBytes;
}
/**
* Appends a hex-encoded representation of the provided character to the given
* buffer. Each byte of the hex-encoded representation will be prefixed with
* a backslash.
*
* @param c The character to be encoded.
* @param buffer The buffer to which the hex-encoded representation should
* be appended.
*/
public static void hexEncode(final char c,
@NotNull final StringBuilder buffer)
{
final byte[] charBytes;
if (c <= 0x7F)
{
charBytes = new byte[] { (byte) (c & 0x7F) };
}
else
{
charBytes = getBytes(String.valueOf(c));
}
for (final byte b : charBytes)
{
buffer.append('\\');
toHex(b, buffer);
}
}
/**
* Appends a hex-encoded representation of the provided code point to the
* given buffer. Each byte of the hex-encoded representation will be prefixed
* with a backslash.
*
* @param codePoint The code point to be encoded.
* @param buffer The buffer to which the hex-encoded representation
* should be appended.
*/
public static void hexEncode(final int codePoint,
@NotNull final StringBuilder buffer)
{
final byte[] charBytes =
getBytes(new String(new int[] { codePoint }, 0, 1));
for (final byte b : charBytes)
{
buffer.append('\\');
toHex(b, buffer);
}
}
/**
* Appends the Java code that may be used to create the provided byte
* array to the given buffer.
*
* @param array The byte array containing the data to represent. It must
* not be {@code null}.
* @param buffer The buffer to which the code should be appended.
*/
public static void byteArrayToCode(@NotNull final byte[] array,
@NotNull final StringBuilder buffer)
{
buffer.append("new byte[] {");
for (int i=0; i < array.length; i++)
{
if (i > 0)
{
buffer.append(',');
}
buffer.append(" (byte) 0x");
toHex(array[i], buffer);
}
buffer.append(" }");
}
/**
* Retrieves a single-line string representation of the stack trace for the
* provided {@code Throwable}. It will include the unqualified name of the
* {@code Throwable} class, a list of source files and line numbers (if
* available) for the stack trace, and will also include the stack trace for
* the cause (if present).
*
* @param t The {@code Throwable} for which to retrieve the stack trace.
*
* @return A single-line string representation of the stack trace for the
* provided {@code Throwable}.
*/
@NotNull()
public static String getStackTrace(@NotNull final Throwable t)
{
final StringBuilder buffer = new StringBuilder();
getStackTrace(t, buffer);
return buffer.toString();
}
/**
* Appends a single-line string representation of the stack trace for the
* provided {@code Throwable} to the given buffer. It will include the
* unqualified name of the {@code Throwable} class, a list of source files and
* line numbers (if available) for the stack trace, and will also include the
* stack trace for the cause (if present).
*
* @param t The {@code Throwable} for which to retrieve the stack
* trace.
* @param buffer The buffer to which the information should be appended.
*/
public static void getStackTrace(@NotNull final Throwable t,
@NotNull final StringBuilder buffer)
{
buffer.append(getUnqualifiedClassName(t.getClass()));
buffer.append('(');
final String message = t.getMessage();
if (message != null)
{
buffer.append("message='");
buffer.append(message);
buffer.append("', ");
}
buffer.append("trace='");
getStackTrace(t.getStackTrace(), buffer);
buffer.append('\'');
final Throwable cause = t.getCause();
if (cause != null)
{
buffer.append(", cause=");
getStackTrace(cause, buffer);
}
final String ldapSDKVersionString = ", ldapSDKVersion=" +
Version.NUMERIC_VERSION_STRING + ", revision=" + Version.REVISION_ID;
if (buffer.indexOf(ldapSDKVersionString) < 0)
{
buffer.append(ldapSDKVersionString);
}
buffer.append(')');
}
/**
* Returns a single-line string representation of the stack trace. It will
* include a list of source files and line numbers (if available) for the
* stack trace.
*
* @param elements The stack trace.
*
* @return A single-line string representation of the stack trace.
*/
@NotNull()
public static String getStackTrace(
@NotNull final StackTraceElement[] elements)
{
final StringBuilder buffer = new StringBuilder();
getStackTrace(elements, buffer);
return buffer.toString();
}
/**
* Appends a single-line string representation of the stack trace to the given
* buffer. It will include a list of source files and line numbers
* (if available) for the stack trace.
*
* @param elements The stack trace.
* @param buffer The buffer to which the information should be appended.
*/
public static void getStackTrace(@NotNull final StackTraceElement[] elements,
@NotNull final StringBuilder buffer)
{
getStackTrace(elements, buffer, -1);
}
/**
* Appends a single-line string representation of the stack trace to the given
* buffer. It will include a list of source files and line numbers
* (if available) for the stack trace.
*
* @param elements The stack trace.
* @param buffer The buffer to which the information should be
* appended.
* @param maxPreSDKFrames The maximum number of stack trace frames to
* include from code invoked before calling into the
* LDAP SDK. A value of zero indicates that only
* stack trace frames from the LDAP SDK itself (or
* things that it calls) will be included. A
* negative value indicates that
*/
public static void getStackTrace(@NotNull final StackTraceElement[] elements,
@NotNull final StringBuilder buffer,
final int maxPreSDKFrames)
{
boolean sdkElementFound = false;
int numPreSDKElementsFound = 0;
for (int i=0; i < elements.length; i++)
{
if (i > 0)
{
buffer.append(" / ");
}
if (elements[i].getClassName().startsWith("com.unboundid."))
{
sdkElementFound = true;
}
else if (sdkElementFound)
{
if ((maxPreSDKFrames >= 0) &&
(numPreSDKElementsFound >= maxPreSDKFrames))
{
buffer.append("...");
return;
}
numPreSDKElementsFound++;
}
buffer.append(elements[i].getMethodName());
buffer.append('(');
buffer.append(elements[i].getFileName());
final int lineNumber = elements[i].getLineNumber();
if (lineNumber > 0)
{
buffer.append(':');
buffer.append(lineNumber);
}
else if (elements[i].isNativeMethod())
{
buffer.append(":native");
}
else
{
buffer.append(":unknown");
}
buffer.append(')');
}
}
/**
* Retrieves a string representation of the provided {@code Throwable} object
* suitable for use in a message. For runtime exceptions and errors, then a
* full stack trace for the exception will be provided. For exception types
* defined in the LDAP SDK, then its {@code getExceptionMessage} method will
* be used to get the string representation. For all other types of
* exceptions, then the standard string representation will be used.
*
* For all types of exceptions, the message will also include the cause if one
* exists.
*
* @param t The {@code Throwable} for which to generate the exception
* message.
*
* @return A string representation of the provided {@code Throwable} object
* suitable for use in a message.
*/
@NotNull()
public static String getExceptionMessage(@NotNull final Throwable t)
{
final boolean includeCause =
Boolean.getBoolean(Debug.PROPERTY_INCLUDE_CAUSE_IN_EXCEPTION_MESSAGES);
final boolean includeStackTrace = Boolean.getBoolean(
Debug.PROPERTY_INCLUDE_STACK_TRACE_IN_EXCEPTION_MESSAGES);
return getExceptionMessage(t, includeCause, includeStackTrace);
}
/**
* Retrieves a string representation of the provided {@code Throwable} object
* suitable for use in a message. For runtime exceptions and errors, then a
* full stack trace for the exception will be provided. For exception types
* defined in the LDAP SDK, then its {@code getExceptionMessage} method will
* be used to get the string representation. For all other types of
* exceptions, then the standard string representation will be used.
*
* For all types of exceptions, the message will also include the cause if one
* exists.
*
* @param t The {@code Throwable} for which to generate the
* exception message.
* @param includeCause Indicates whether to include information about
* the cause (if any) in the exception message.
* @param includeStackTrace Indicates whether to include a condensed
* representation of the stack trace in the
* exception message.
*
* @return A string representation of the provided {@code Throwable} object
* suitable for use in a message.
*/
@NotNull()
public static String getExceptionMessage(@Nullable final Throwable t,
final boolean includeCause,
final boolean includeStackTrace)
{
if (t == null)
{
return ERR_NO_EXCEPTION.get();
}
final StringBuilder buffer = new StringBuilder();
if (t instanceof LDAPSDKException)
{
buffer.append(((LDAPSDKException) t).getExceptionMessage());
}
else if (t instanceof LDAPSDKRuntimeException)
{
buffer.append(((LDAPSDKRuntimeException) t).getExceptionMessage());
}
else if (t instanceof NullPointerException)
{
// For NullPointerExceptions, we'll always print at least a portion of
// the stack trace that includes all of the LDAP SDK code, and up to
// three frames of whatever called into the SDK.
buffer.append("NullPointerException(");
getStackTrace(t.getStackTrace(), buffer, 3);
buffer.append(')');
}
else if ((t.getMessage() == null) || t.getMessage().isEmpty() ||
t.getMessage().equalsIgnoreCase("null"))
{
getStackTrace(t, buffer);
}
else
{
buffer.append(t.getClass().getSimpleName());
buffer.append('(');
buffer.append(t.getMessage());
buffer.append(')');
if (includeStackTrace)
{
buffer.append(" trace=");
getStackTrace(t, buffer);
}
else if (includeCause)
{
final Throwable cause = t.getCause();
if (cause != null)
{
buffer.append(" caused by ");
buffer.append(getExceptionMessage(cause));
}
}
}
final String ldapSDKVersionString = ", ldapSDKVersion=" +
Version.NUMERIC_VERSION_STRING + ", revision=" + Version.REVISION_ID;
if (buffer.indexOf(ldapSDKVersionString) < 0)
{
buffer.append(ldapSDKVersionString);
}
return buffer.toString();
}
/**
* Retrieves the unqualified name (i.e., the name without package information)
* for the provided class.
*
* @param c The class for which to retrieve the unqualified name.
*
* @return The unqualified name for the provided class.
*/
@NotNull()
public static String getUnqualifiedClassName(@NotNull final Class> c)
{
final String className = c.getName();
final int lastPeriodPos = className.lastIndexOf('.');
if (lastPeriodPos > 0)
{
return className.substring(lastPeriodPos+1);
}
else
{
return className;
}
}
/**
* Retrieves a {@code TimeZone} object that represents the UTC (universal
* coordinated time) time zone.
*
* @return A {@code TimeZone} object that represents the UTC time zone.
*/
@NotNull()
public static TimeZone getUTCTimeZone()
{
return UTC_TIME_ZONE;
}
/**
* Encodes the provided timestamp in generalized time format.
*
* @param timestamp The timestamp to be encoded in generalized time format.
* It should use the same format as the
* {@code System.currentTimeMillis()} method (i.e., the
* number of milliseconds since 12:00am UTC on January 1,
* 1970).
*
* @return The generalized time representation of the provided date.
*/
@NotNull()
public static String encodeGeneralizedTime(final long timestamp)
{
return encodeGeneralizedTime(new Date(timestamp));
}
/**
* Encodes the provided date in generalized time format.
*
* @param d The date to be encoded in generalized time format.
*
* @return The generalized time representation of the provided date.
*/
@NotNull()
public static String encodeGeneralizedTime(@NotNull final Date d)
{
SimpleDateFormat dateFormat = GENERALIZED_TIME_FORMATTERS.get();
if (dateFormat == null)
{
dateFormat = new SimpleDateFormat("yyyyMMddHHmmss.SSS'Z'");
dateFormat.setTimeZone(UTC_TIME_ZONE);
GENERALIZED_TIME_FORMATTERS.set(dateFormat);
}
return dateFormat.format(d);
}
/**
* Decodes the provided string as a timestamp in generalized time format.
*
* @param t The timestamp to be decoded. It must not be {@code null}.
*
* @return The {@code Date} object decoded from the provided timestamp.
*
* @throws ParseException If the provided string could not be decoded as a
* timestamp in generalized time format.
*/
@NotNull()
public static Date decodeGeneralizedTime(@NotNull final String t)
throws ParseException
{
Validator.ensureNotNull(t);
// Extract the time zone information from the end of the value.
int tzPos;
final TimeZone tz;
if (t.endsWith("Z"))
{
tz = TimeZone.getTimeZone("UTC");
tzPos = t.length() - 1;
}
else
{
tzPos = t.lastIndexOf('-');
if (tzPos < 0)
{
tzPos = t.lastIndexOf('+');
if (tzPos < 0)
{
throw new ParseException(ERR_GENTIME_DECODE_CANNOT_PARSE_TZ.get(t),
0);
}
}
tz = TimeZone.getTimeZone("GMT" + t.substring(tzPos));
if (tz.getRawOffset() == 0)
{
// This is the default time zone that will be returned if the value
// cannot be parsed. If it's valid, then it will end in "+0000" or
// "-0000". Otherwise, it's invalid and GMT was just a fallback.
if (! (t.endsWith("+0000") || t.endsWith("-0000")))
{
throw new ParseException(ERR_GENTIME_DECODE_CANNOT_PARSE_TZ.get(t),
tzPos);
}
}
}
// See if the timestamp has a sub-second portion. Note that if there is a
// sub-second portion, then we may need to massage the value so that there
// are exactly three sub-second characters so that it can be interpreted as
// milliseconds.
final String subSecFormatStr;
final String trimmedTimestamp;
int periodPos = t.lastIndexOf('.', tzPos);
if (periodPos > 0)
{
final int subSecondLength = tzPos - periodPos - 1;
switch (subSecondLength)
{
case 0:
subSecFormatStr = "";
trimmedTimestamp = t.substring(0, periodPos);
break;
case 1:
subSecFormatStr = ".SSS";
trimmedTimestamp = t.substring(0, (periodPos+2)) + "00";
break;
case 2:
subSecFormatStr = ".SSS";
trimmedTimestamp = t.substring(0, (periodPos+3)) + '0';
break;
default:
subSecFormatStr = ".SSS";
trimmedTimestamp = t.substring(0, periodPos+4);
break;
}
}
else
{
subSecFormatStr = "";
periodPos = tzPos;
trimmedTimestamp = t.substring(0, tzPos);
}
// Look at where the period is (or would be if it existed) to see how many
// characters are in the integer portion. This will give us what we need
// for the rest of the format string.
final String formatStr;
switch (periodPos)
{
case 10:
formatStr = "yyyyMMddHH" + subSecFormatStr;
break;
case 12:
formatStr = "yyyyMMddHHmm" + subSecFormatStr;
break;
case 14:
formatStr = "yyyyMMddHHmmss" + subSecFormatStr;
break;
default:
throw new ParseException(ERR_GENTIME_CANNOT_PARSE_INVALID_LENGTH.get(t),
periodPos);
}
// We should finally be able to create an appropriate date format object
// to parse the trimmed version of the timestamp.
final SimpleDateFormat dateFormat = new SimpleDateFormat(formatStr);
dateFormat.setTimeZone(tz);
dateFormat.setLenient(false);
return dateFormat.parse(trimmedTimestamp);
}
/**
* Encodes the provided timestamp to the ISO 8601 format described in RFC
* 3339.
*
* @param timestamp The timestamp to be encoded in the RFC 3339 format.
* It should use the same format as the
* {@code System.currentTimeMillis()} method (i.e., the
* number of milliseconds since 12:00am UTC on January 1,
* 1970).
*
* @return The RFC 3339 representation of the provided date.
*/
@NotNull()
public static String encodeRFC3339Time(final long timestamp)
{
return encodeRFC3339Time(new Date(timestamp));
}
/**
* Encodes the provided timestamp to the ISO 8601 format described in RFC
* 3339.
*
* @param d The date to be encoded in the RFC 3339 format.
*
* @return The RFC 3339 representation of the provided date.
*/
@NotNull()
public static String encodeRFC3339Time(@NotNull final Date d)
{
SimpleDateFormat dateFormat = RFC_3339_TIME_FORMATTERS.get();
if (dateFormat == null)
{
dateFormat = new SimpleDateFormat("yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSS'Z'");
dateFormat.setTimeZone(UTC_TIME_ZONE);
RFC_3339_TIME_FORMATTERS.set(dateFormat);
}
return dateFormat.format(d);
}
/**
* Decodes the provided string as a timestamp encoded in the ISO 8601 format
* described in RFC 3339.
*
* @param timestamp The timestamp to be decoded in the RFC 3339 format.
*
* @return The {@code Date} object decoded from the provided timestamp.
*
* @throws ParseException If the provided string could not be decoded as a
* timestamp in the RFC 3339 time format.
*/
@NotNull()
public static Date decodeRFC3339Time(@NotNull final String timestamp)
throws ParseException
{
// Make sure that the string representation has the minimum acceptable
// length.
if (timestamp.length() < 20)
{
throw new ParseException(ERR_RFC_3339_TIME_TOO_SHORT.get(timestamp), 0);
}
// Parse the year, month, day, hour, minute, and second components from the
// timestamp, and make sure the appropriate separator characters are between
// those components.
final int year = parseRFC3339Number(timestamp, 0, 4);
validateRFC3339TimestampSeparatorCharacter(timestamp, 4, '-');
final int month = parseRFC3339Number(timestamp, 5, 2);
validateRFC3339TimestampSeparatorCharacter(timestamp, 7, '-');
final int day = parseRFC3339Number(timestamp, 8, 2);
validateRFC3339TimestampSeparatorCharacter(timestamp, 10, 'T');
final int hour = parseRFC3339Number(timestamp, 11, 2);
validateRFC3339TimestampSeparatorCharacter(timestamp, 13, ':');
final int minute = parseRFC3339Number(timestamp, 14, 2);
validateRFC3339TimestampSeparatorCharacter(timestamp, 16, ':');
final int second = parseRFC3339Number(timestamp, 17, 2);
// Make sure that the month and day values are acceptable.
switch (month)
{
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
// January, March, May, July, August, October, and December all have 31
// days.
if ((day < 1) || (day > 31))
{
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_DAY_FOR_MONTH.get(timestamp, day,
month),
8);
}
break;
case 4:
case 6:
case 9:
case 11:
// April, June, September, and November all have 30 days.
if ((day < 1) || (day > 30))
{
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_DAY_FOR_MONTH.get(timestamp, day,
month),
8);
}
break;
case 2:
// February can have 28 or 29 days, depending on whether it's a leap
// year. Although we could determine whether the provided year is a
// leap year, we'll just always accept up to 29 days for February.
if ((day < 1) || (day > 29))
{
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_DAY_FOR_MONTH.get(timestamp, day,
month),
8);
}
break;
default:
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_MONTH.get(timestamp, month), 5);
}
// Make sure that the hour, minute, and second values are acceptable. Note
// that while ISO 8601 permits a value of 24 for the hour, RFC 3339 only
// permits hour values between 0 and 23. Also note that some minutes can
// have up to 61 seconds for leap seconds, so we'll always account for that.
if ((hour < 0) || (hour > 23))
{
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_HOUR.get(timestamp, hour), 11);
}
if ((minute < 0) || (minute > 59))
{
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_MINUTE.get(timestamp, minute), 14);
}
if ((second < 0) || (second > 60))
{
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_SECOND.get(timestamp, second), 17);
}
// See if there is a sub-second portion. If so, then there will be a
// period at position 19 followed by at least one digit. This
// implementation will only support timestamps with no more than three
// sub-second digits.
int milliseconds = 0;
int timeZoneStartPos = -1;
if (timestamp.charAt(19) == '.')
{
int numDigits = 0;
final StringBuilder subSecondString = new StringBuilder(3);
for (int pos=20; pos < timestamp.length(); pos++)
{
final char c = timestamp.charAt(pos);
switch (c)
{
case '0':
numDigits++;
if (subSecondString.length() > 0)
{
// Only add a zero if it's not the first digit.
subSecondString.append(c);
}
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
numDigits++;
subSecondString.append(c);
break;
case 'Z':
case '+':
case '-':
timeZoneStartPos = pos;
break;
default:
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_SUB_SECOND_CHAR.get(timestamp, c,
pos),
pos);
}
if (timeZoneStartPos > 0)
{
break;
}
if (numDigits > 3)
{
throw new ParseException(
ERR_RFC_3339_TIME_TOO_MANY_SUB_SECOND_DIGITS.get(timestamp),
20);
}
}
if (timeZoneStartPos < 0)
{
throw new ParseException(
ERR_RFC_3339_TIME_MISSING_TIME_ZONE_AFTER_SUB_SECOND.get(
timestamp),
(timestamp.length() - 1));
}
if (numDigits == 0)
{
throw new ParseException(
ERR_RFC_3339_TIME_NO_SUB_SECOND_DIGITS.get(timestamp), 19);
}
if (subSecondString.length() == 0)
{
// This is possible if the sub-second portion is all zeroes.
subSecondString.append('0');
}
milliseconds = Integer.parseInt(subSecondString.toString());
if (numDigits == 1)
{
milliseconds *= 100;
}
else if (numDigits == 2)
{
milliseconds *= 10;
}
}
else
{
timeZoneStartPos = 19;
}
// The remainder of the timestamp should be the time zone.
final TimeZone timeZone;
if (timestamp.substring(timeZoneStartPos).equals("Z"))
{
// This is shorthand for the UTC time zone.
timeZone = UTC_TIME_ZONE;
}
else
{
// This is an offset from UTC, which should be in the form "+HH:MM" or
// "-HH:MM". Make sure it has the expected length.
if ((timestamp.length() - timeZoneStartPos) != 6)
{
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_TZ.get(timestamp), timeZoneStartPos);
}
// Make sure it starts with "+" or "-".
final int firstChar = timestamp.charAt(timeZoneStartPos);
if ((firstChar != '+') && (firstChar != '-'))
{
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_TZ.get(timestamp), timeZoneStartPos);
}
// Make sure the hour offset is valid.
final int timeZoneHourOffset =
parseRFC3339Number(timestamp, (timeZoneStartPos+1), 2);
if ((timeZoneHourOffset < 0) || (timeZoneHourOffset > 23))
{
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_TZ.get(timestamp), timeZoneStartPos);
}
// Make sure there is a colon between the hour and the minute portions of
// the offset.
if (timestamp.charAt(timeZoneStartPos+3) != ':')
{
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_TZ.get(timestamp), timeZoneStartPos);
}
final int timeZoneMinuteOffset =
parseRFC3339Number(timestamp, (timeZoneStartPos+4), 2);
if ((timeZoneMinuteOffset < 0) || (timeZoneMinuteOffset > 59))
{
throw new ParseException(
ERR_RFC_3339_TIME_INVALID_TZ.get(timestamp), timeZoneStartPos);
}
timeZone = TimeZone.getTimeZone(
"GMT" + timestamp.substring(timeZoneStartPos));
}
// Put everything together to construct the appropriate date.
final GregorianCalendar calendar =
new GregorianCalendar(year,
(month-1), // NOTE: Calendar stupidly uses zero-indexed months.
day, hour, minute, second);
calendar.set(GregorianCalendar.MILLISECOND, milliseconds);
calendar.setTimeZone(timeZone);
return calendar.getTime();
}
/**
* Ensures that the provided timestamp string has the expected character at
* the specified position.
*
* @param timestamp The timestamp to examine.
* It must not be {@code null}.
* @param pos The position of the character to examine.
* @param expectedChar The character expected at the specified position.
*
* @throws ParseException If the provided timestamp does not have the
* expected
*/
private static void validateRFC3339TimestampSeparatorCharacter(
@NotNull final String timestamp, final int pos,
final char expectedChar)
throws ParseException
{
if (timestamp.charAt(pos) != expectedChar)
{
throw new ParseException(
ERR_RFC_3339_INVALID_SEPARATOR.get(timestamp, timestamp.charAt(pos),
pos, expectedChar),
pos);
}
}
/**
* Parses the number at the specified location in the timestamp.
*
* @param timestamp The timestamp to examine. It must not be {@code null}.
* @param pos The position at which to begin parsing the number.
* @param numDigits The number of digits in the number.
*
* @return The number parsed from the provided timestamp.
*
* @throws ParseException If a problem is encountered while trying to parse
* the number from the timestamp.
*/
private static int parseRFC3339Number(@NotNull final String timestamp,
final int pos, final int numDigits)
throws ParseException
{
int value = 0;
for (int i=0; i < numDigits; i++)
{
value *= 10;
switch (timestamp.charAt(pos+i))
{
case '0':
break;
case '1':
value += 1;
break;
case '2':
value += 2;
break;
case '3':
value += 3;
break;
case '4':
value += 4;
break;
case '5':
value += 5;
break;
case '6':
value += 6;
break;
case '7':
value += 7;
break;
case '8':
value += 8;
break;
case '9':
value += 9;
break;
default:
throw new ParseException(
ERR_RFC_3339_INVALID_DIGIT.get(timestamp,
timestamp.charAt(pos+i), (pos+i)),
(pos+i));
}
}
return value;
}
/**
* Trims only leading spaces from the provided string, leaving any trailing
* spaces intact.
*
* @param s The string to be processed. It must not be {@code null}.
*
* @return The original string if no trimming was required, or a new string
* without leading spaces if the provided string had one or more. It
* may be an empty string if the provided string was an empty string
* or contained only spaces.
*/
@NotNull()
public static String trimLeading(@NotNull final String s)
{
Validator.ensureNotNull(s);
int nonSpacePos = 0;
final int length = s.length();
while ((nonSpacePos < length) && (s.charAt(nonSpacePos) == ' '))
{
nonSpacePos++;
}
if (nonSpacePos == 0)
{
// There were no leading spaces.
return s;
}
else if (nonSpacePos >= length)
{
// There were no non-space characters.
return "";
}
else
{
// There were leading spaces, so return the string without them.
return s.substring(nonSpacePos, length);
}
}
/**
* Trims only trailing spaces from the provided string, leaving any leading
* spaces intact.
*
* @param s The string to be processed. It must not be {@code null}.
*
* @return The original string if no trimming was required, or a new string
* without trailing spaces if the provided string had one or more.
* It may be an empty string if the provided string was an empty
* string or contained only spaces.
*/
@NotNull()
public static String trimTrailing(@NotNull final String s)
{
Validator.ensureNotNull(s);
final int lastPos = s.length() - 1;
int nonSpacePos = lastPos;
while ((nonSpacePos >= 0) && (s.charAt(nonSpacePos) == ' '))
{
nonSpacePos--;
}
if (nonSpacePos < 0)
{
// There were no non-space characters.
return "";
}
else if (nonSpacePos == lastPos)
{
// There were no trailing spaces.
return s;
}
else
{
// There were trailing spaces, so return the string without them.
return s.substring(0, (nonSpacePos+1));
}
}
/**
* Wraps the contents of the specified line using the given width. It will
* attempt to wrap at spaces to preserve words, but if that is not possible
* (because a single "word" is longer than the maximum width), then it will
* wrap in the middle of the word at the specified maximum width.
*
* @param line The line to be wrapped. It must not be {@code null}.
* @param maxWidth The maximum width for lines in the resulting list. A
* value less than or equal to zero will cause no wrapping
* to be performed.
*
* @return A list of the wrapped lines. It may be empty if the provided line
* contained only spaces.
*/
@NotNull()
public static List wrapLine(@NotNull final String line,
final int maxWidth)
{
return wrapLine(line, maxWidth, maxWidth);
}
/**
* Wraps the contents of the specified line using the given width. It will
* attempt to wrap at spaces to preserve words, but if that is not possible
* (because a single "word" is longer than the maximum width), then it will
* wrap in the middle of the word at the specified maximum width.
*
* @param line The line to be wrapped. It must not be
* {@code null}.
* @param maxFirstLineWidth The maximum length for the first line in
* the resulting list. A value less than or
* equal to zero will cause no wrapping to be
* performed.
* @param maxSubsequentLineWidth The maximum length for all lines except the
* first line. This must be greater than zero
* unless {@code maxFirstLineWidth} is less
* than or equal to zero.
*
* @return A list of the wrapped lines. It may be empty if the provided line
* contained only spaces.
*/
@NotNull()
public static List wrapLine(@NotNull final String line,
final int maxFirstLineWidth,
final int maxSubsequentLineWidth)
{
if (maxFirstLineWidth > 0)
{
Validator.ensureTrue(maxSubsequentLineWidth > 0);
}
// See if the provided string already contains line breaks. If so, then
// treat it as multiple lines rather than a single line.
final int breakPos = line.indexOf('\n');
if (breakPos >= 0)
{
final ArrayList lineList = new ArrayList<>(10);
final StringTokenizer tokenizer = new StringTokenizer(line, "\r\n");
while (tokenizer.hasMoreTokens())
{
lineList.addAll(wrapLine(tokenizer.nextToken(), maxFirstLineWidth,
maxSubsequentLineWidth));
}
return lineList;
}
final int length = line.length();
if ((maxFirstLineWidth <= 0) || (length < maxFirstLineWidth))
{
return Collections.singletonList(line);
}
int wrapPos = maxFirstLineWidth;
int lastWrapPos = 0;
final ArrayList lineList = new ArrayList<>(5);
while (true)
{
final int spacePos = line.lastIndexOf(' ', wrapPos);
if (spacePos > lastWrapPos)
{
// We found a space in an acceptable location, so use it after trimming
// any trailing spaces.
final String s = trimTrailing(line.substring(lastWrapPos, spacePos));
// Don't bother adding the line if it contained only spaces.
if (! s.isEmpty())
{
lineList.add(s);
}
wrapPos = spacePos;
}
else
{
// We didn't find any spaces, so we'll have to insert a hard break at
// the specified wrap column.
lineList.add(line.substring(lastWrapPos, wrapPos));
}
// Skip over any spaces before the next non-space character.
while ((wrapPos < length) && (line.charAt(wrapPos) == ' '))
{
wrapPos++;
}
lastWrapPos = wrapPos;
wrapPos += maxSubsequentLineWidth;
if (wrapPos >= length)
{
// The last fragment can fit on the line, so we can handle that now and
// break.
if (lastWrapPos >= length)
{
break;
}
else
{
final String s = line.substring(lastWrapPos);
lineList.add(s);
break;
}
}
}
return lineList;
}
/**
* This method returns a form of the provided argument that is safe to
* use on the command line for the local platform. This method is provided as
* a convenience wrapper around {@link ExampleCommandLineArgument}. Calling
* this method is equivalent to:
*
*
* return ExampleCommandLineArgument.getCleanArgument(s).getLocalForm();
*
*
* For getting direct access to command line arguments that are safe to
* use on other platforms, call
* {@link ExampleCommandLineArgument#getCleanArgument}.
*
* @param s The string to be processed. It must not be {@code null}.
*
* @return A cleaned version of the provided string in a form that will allow
* it to be displayed as the value of a command-line argument on.
*/
@NotNull()
public static String cleanExampleCommandLineArgument(@NotNull final String s)
{
return ExampleCommandLineArgument.getCleanArgument(s).getLocalForm();
}
/**
* Retrieves a single string which is a concatenation of all of the provided
* strings.
*
* @param a The array of strings to concatenate. It must not be
* {@code null} but may be empty.
*
* @return A string containing a concatenation of all of the strings in the
* provided array.
*/
@NotNull()
public static String concatenateStrings(@NotNull final String... a)
{
return concatenateStrings(null, null, " ", null, null, a);
}
/**
* Retrieves a single string which is a concatenation of all of the provided
* strings.
*
* @param l The list of strings to concatenate. It must not be
* {@code null} but may be empty.
*
* @return A string containing a concatenation of all of the strings in the
* provided list.
*/
@NotNull()
public static String concatenateStrings(@NotNull final List l)
{
return concatenateStrings(null, null, " ", null, null, l);
}
/**
* Retrieves a single string which is a concatenation of all of the provided
* strings.
*
* @param beforeList A string that should be placed at the beginning of
* the list. It may be {@code null} or empty if
* nothing should be placed at the beginning of the
* list.
* @param beforeElement A string that should be placed before each element
* in the list. It may be {@code null} or empty if
* nothing should be placed before each element.
* @param betweenElements The separator that should be placed between
* elements in the list. It may be {@code null} or
* empty if no separator should be placed between
* elements.
* @param afterElement A string that should be placed after each element
* in the list. It may be {@code null} or empty if
* nothing should be placed after each element.
* @param afterList A string that should be placed at the end of the
* list. It may be {@code null} or empty if nothing
* should be placed at the end of the list.
* @param a The array of strings to concatenate. It must not
* be {@code null} but may be empty.
*
* @return A string containing a concatenation of all of the strings in the
* provided list.
*/
@NotNull()
public static String concatenateStrings(@Nullable final String beforeList,
@Nullable final String beforeElement,
@Nullable final String betweenElements,
@Nullable final String afterElement,
@Nullable final String afterList,
@NotNull final String... a)
{
return concatenateStrings(beforeList, beforeElement, betweenElements,
afterElement, afterList, Arrays.asList(a));
}
/**
* Retrieves a single string which is a concatenation of all of the provided
* strings.
*
* @param beforeList A string that should be placed at the beginning of
* the list. It may be {@code null} or empty if
* nothing should be placed at the beginning of the
* list.
* @param beforeElement A string that should be placed before each element
* in the list. It may be {@code null} or empty if
* nothing should be placed before each element.
* @param betweenElements The separator that should be placed between
* elements in the list. It may be {@code null} or
* empty if no separator should be placed between
* elements.
* @param afterElement A string that should be placed after each element
* in the list. It may be {@code null} or empty if
* nothing should be placed after each element.
* @param afterList A string that should be placed at the end of the
* list. It may be {@code null} or empty if nothing
* should be placed at the end of the list.
* @param l The list of strings to concatenate. It must not
* be {@code null} but may be empty.
*
* @return A string containing a concatenation of all of the strings in the
* provided list.
*/
@NotNull()
public static String concatenateStrings(@Nullable final String beforeList,
@Nullable final String beforeElement,
@Nullable final String betweenElements,
@Nullable final String afterElement,
@Nullable final String afterList,
@NotNull final List l)
{
Validator.ensureNotNull(l);
final StringBuilder buffer = new StringBuilder();
if (beforeList != null)
{
buffer.append(beforeList);
}
final Iterator iterator = l.iterator();
while (iterator.hasNext())
{
if (beforeElement != null)
{
buffer.append(beforeElement);
}
buffer.append(iterator.next());
if (afterElement != null)
{
buffer.append(afterElement);
}
if ((betweenElements != null) && iterator.hasNext())
{
buffer.append(betweenElements);
}
}
if (afterList != null)
{
buffer.append(afterList);
}
return buffer.toString();
}
/**
* Converts a duration in seconds to a string with a human-readable duration
* which may include days, hours, minutes, and seconds, to the extent that
* they are needed.
*
* @param s The number of seconds to be represented.
*
* @return A string containing a human-readable representation of the
* provided time.
*/
@NotNull()
public static String secondsToHumanReadableDuration(final long s)
{
return millisToHumanReadableDuration(s * 1000L);
}
/**
* Converts a duration in seconds to a string with a human-readable duration
* which may include days, hours, minutes, and seconds, to the extent that
* they are needed.
*
* @param m The number of milliseconds to be represented.
*
* @return A string containing a human-readable representation of the
* provided time.
*/
@NotNull()
public static String millisToHumanReadableDuration(final long m)
{
final StringBuilder buffer = new StringBuilder();
long numMillis = m;
final long numDays = numMillis / 86_400_000L;
if (numDays > 0)
{
numMillis -= (numDays * 86_400_000L);
if (numDays == 1)
{
buffer.append(INFO_NUM_DAYS_SINGULAR.get(numDays));
}
else
{
buffer.append(INFO_NUM_DAYS_PLURAL.get(numDays));
}
}
final long numHours = numMillis / 3_600_000L;
if (numHours > 0)
{
numMillis -= (numHours * 3_600_000L);
if (buffer.length() > 0)
{
buffer.append(", ");
}
if (numHours == 1)
{
buffer.append(INFO_NUM_HOURS_SINGULAR.get(numHours));
}
else
{
buffer.append(INFO_NUM_HOURS_PLURAL.get(numHours));
}
}
final long numMinutes = numMillis / 60_000L;
if (numMinutes > 0)
{
numMillis -= (numMinutes * 60_000L);
if (buffer.length() > 0)
{
buffer.append(", ");
}
if (numMinutes == 1)
{
buffer.append(INFO_NUM_MINUTES_SINGULAR.get(numMinutes));
}
else
{
buffer.append(INFO_NUM_MINUTES_PLURAL.get(numMinutes));
}
}
if (numMillis == 1000)
{
if (buffer.length() > 0)
{
buffer.append(", ");
}
buffer.append(INFO_NUM_SECONDS_SINGULAR.get(1));
}
else if ((numMillis > 0) || (buffer.length() == 0))
{
if (buffer.length() > 0)
{
buffer.append(", ");
}
final long numSeconds = numMillis / 1000L;
numMillis -= (numSeconds * 1000L);
if ((numMillis % 1000L) != 0L)
{
final double numSecondsDouble = numSeconds + (numMillis / 1000.0);
final DecimalFormat decimalFormat = new DecimalFormat("0.000");
buffer.append(INFO_NUM_SECONDS_WITH_DECIMAL.get(
decimalFormat.format(numSecondsDouble)));
}
else
{
buffer.append(INFO_NUM_SECONDS_PLURAL.get(numSeconds));
}
}
return buffer.toString();
}
/**
* Converts the provided number of nanoseconds to milliseconds.
*
* @param nanos The number of nanoseconds to convert to milliseconds.
*
* @return The number of milliseconds that most closely corresponds to the
* specified number of nanoseconds.
*/
public static long nanosToMillis(final long nanos)
{
return Math.max(0L, Math.round(nanos / 1_000_000.0d));
}
/**
* Converts the provided number of milliseconds to nanoseconds.
*
* @param millis The number of milliseconds to convert to nanoseconds.
*
* @return The number of nanoseconds that most closely corresponds to the
* specified number of milliseconds.
*/
public static long millisToNanos(final long millis)
{
return Math.max(0L, (millis * 1_000_000L));
}
/**
* Indicates whether the provided string is a valid numeric OID. A numeric
* OID must start and end with a digit, must have at least on period, must
* contain only digits and periods, and must not have two consecutive periods.
*
* @param s The string to examine. It must not be {@code null}.
*
* @return {@code true} if the provided string is a valid numeric OID, or
* {@code false} if not.
*/
public static boolean isNumericOID(@NotNull final String s)
{
boolean digitRequired = true;
boolean periodFound = false;
for (final char c : s.toCharArray())
{
switch (c)
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
digitRequired = false;
break;
case '.':
if (digitRequired)
{
return false;
}
else
{
digitRequired = true;
}
periodFound = true;
break;
default:
return false;
}
}
return (periodFound && (! digitRequired));
}
/**
* Capitalizes the provided string. The first character will be converted to
* uppercase, and the rest of the string will be left unaltered.
*
* @param s The string to be capitalized.
*
* @return A capitalized version of the provided string, or {@code null} if
* the provided string was {@code null}.
*/
@Nullable()
public static String capitalize(@Nullable final String s)
{
return capitalize(s, false);
}
/**
* Capitalizes the provided string. The first character of the string (or
* optionally the first character of each word in the string)
*
* @param s The string to be capitalized.
* @param allWords Indicates whether to capitalize all words in the string,
* or only the first word.
*
* @return A capitalized version of the provided string, or {@code null} if
* the provided string was {@code null}.
*/
@Nullable()
public static String capitalize(@Nullable final String s,
final boolean allWords)
{
if (s == null)
{
return null;
}
switch (s.length())
{
case 0:
return s;
case 1:
return s.toUpperCase();
default:
boolean capitalize = true;
final char[] chars = s.toCharArray();
final StringBuilder buffer = new StringBuilder(chars.length);
for (final char c : chars)
{
// Whitespace and punctuation will be considered word breaks.
if (Character.isWhitespace(c) ||
(((c >= '!') && (c <= '.')) ||
((c >= ':') && (c <= '@')) ||
((c >= '[') && (c <= '`')) ||
((c >= '{') && (c <= '~'))))
{
buffer.append(c);
capitalize |= allWords;
}
else if (capitalize)
{
buffer.append(Character.toUpperCase(c));
capitalize = false;
}
else
{
buffer.append(c);
}
}
return buffer.toString();
}
}
/**
* Encodes the provided UUID to a byte array containing its 128-bit
* representation.
*
* @param uuid The UUID to be encoded. It must not be {@code null}.
*
* @return The byte array containing the 128-bit encoded UUID.
*/
@NotNull()
public static byte[] encodeUUID(@NotNull final UUID uuid)
{
final byte[] b = new byte[16];
final long mostSignificantBits = uuid.getMostSignificantBits();
b[0] = (byte) ((mostSignificantBits >> 56) & 0xFF);
b[1] = (byte) ((mostSignificantBits >> 48) & 0xFF);
b[2] = (byte) ((mostSignificantBits >> 40) & 0xFF);
b[3] = (byte) ((mostSignificantBits >> 32) & 0xFF);
b[4] = (byte) ((mostSignificantBits >> 24) & 0xFF);
b[5] = (byte) ((mostSignificantBits >> 16) & 0xFF);
b[6] = (byte) ((mostSignificantBits >> 8) & 0xFF);
b[7] = (byte) (mostSignificantBits & 0xFF);
final long leastSignificantBits = uuid.getLeastSignificantBits();
b[8] = (byte) ((leastSignificantBits >> 56) & 0xFF);
b[9] = (byte) ((leastSignificantBits >> 48) & 0xFF);
b[10] = (byte) ((leastSignificantBits >> 40) & 0xFF);
b[11] = (byte) ((leastSignificantBits >> 32) & 0xFF);
b[12] = (byte) ((leastSignificantBits >> 24) & 0xFF);
b[13] = (byte) ((leastSignificantBits >> 16) & 0xFF);
b[14] = (byte) ((leastSignificantBits >> 8) & 0xFF);
b[15] = (byte) (leastSignificantBits & 0xFF);
return b;
}
/**
* Decodes the value of the provided byte array as a Java UUID.
*
* @param b The byte array to be decoded as a UUID. It must not be
* {@code null}.
*
* @return The decoded UUID.
*
* @throws ParseException If the provided byte array cannot be parsed as a
* UUID.
*/
@NotNull()
public static UUID decodeUUID(@NotNull final byte[] b)
throws ParseException
{
if (b.length != 16)
{
throw new ParseException(ERR_DECODE_UUID_INVALID_LENGTH.get(toHex(b)), 0);
}
long mostSignificantBits = 0L;
for (int i=0; i < 8; i++)
{
mostSignificantBits = (mostSignificantBits << 8) | (b[i] & 0xFF);
}
long leastSignificantBits = 0L;
for (int i=8; i < 16; i++)
{
leastSignificantBits = (leastSignificantBits << 8) | (b[i] & 0xFF);
}
return new UUID(mostSignificantBits, leastSignificantBits);
}
/**
* Returns {@code true} if and only if the current process is running on
* a Windows-based operating system.
*
* @return {@code true} if the current process is running on a Windows-based
* operating system and {@code false} otherwise.
*/
public static boolean isWindows()
{
final String osName = toLowerCase(getSystemProperty("os.name"));
return ((osName != null) && osName.contains("windows"));
}
/**
* Retrieves the string that should be appended to the end of all but the last
* line of a multi-line command to indicate that the command continues onto
* the next line.
*
* This will be the caret (also called a circumflex accent) character on
* Windows systems, and a backslash (also called a reverse solidus) character
* on Linux and UNIX-based systems.
*
* The string value that is returned will not include a space, but it should
* generally be preceded by one or more space to separate it from the previous
* component on the command line.
*
* @return The string that should be appended (generally after one or more
* spaces to separate it from the previous component) to the end of
* all but the last line of a multi-line command to indicate that the
* command continues onto the next line.
*/
@NotNull()
public static String getCommandLineContinuationString()
{
if (isWindows())
{
return "^";
}
else
{
return "\\";
}
}
/**
* Attempts to parse the contents of the provided string to an argument list
* (e.g., converts something like "--arg1 arg1value --arg2 --arg3 arg3value"
* to a list of "--arg1", "arg1value", "--arg2", "--arg3", "arg3value").
*
* @param s The string to be converted to an argument list.
*
* @return The parsed argument list.
*
* @throws ParseException If a problem is encountered while attempting to
* parse the given string to an argument list.
*/
@NotNull()
public static List toArgumentList(@Nullable final String s)
throws ParseException
{
if ((s == null) || s.isEmpty())
{
return Collections.emptyList();
}
int quoteStartPos = -1;
boolean inEscape = false;
final ArrayList argList = new ArrayList<>(20);
final StringBuilder currentArg = new StringBuilder();
for (int i=0; i < s.length(); i++)
{
final char c = s.charAt(i);
if (inEscape)
{
currentArg.append(c);
inEscape = false;
continue;
}
if (c == '\\')
{
inEscape = true;
}
else if (c == '"')
{
if (quoteStartPos >= 0)
{
quoteStartPos = -1;
}
else
{
quoteStartPos = i;
}
}
else if (c == ' ')
{
if (quoteStartPos >= 0)
{
currentArg.append(c);
}
else if (currentArg.length() > 0)
{
argList.add(currentArg.toString());
currentArg.setLength(0);
}
}
else
{
currentArg.append(c);
}
}
if (s.endsWith("\\") && (! s.endsWith("\\\\")))
{
throw new ParseException(ERR_ARG_STRING_DANGLING_BACKSLASH.get(),
(s.length() - 1));
}
if (quoteStartPos >= 0)
{
throw new ParseException(ERR_ARG_STRING_UNMATCHED_QUOTE.get(
quoteStartPos), quoteStartPos);
}
if (currentArg.length() > 0)
{
argList.add(currentArg.toString());
}
return Collections.unmodifiableList(argList);
}
/**
* Retrieves an array containing the elements of the provided collection.
*
* @param The type of element included in the provided
* collection.
* @param collection The collection to convert to an array.
* @param type The type of element contained in the collection.
*
* @return An array containing the elements of the provided list, or
* {@code null} if the provided list is {@code null}.
*/
@Nullable()
public static T[] toArray(@Nullable final Collection collection,
@NotNull final Class type)
{
if (collection == null)
{
return null;
}
@SuppressWarnings("unchecked")
final T[] array = (T[]) Array.newInstance(type, collection.size());
return collection.toArray(array);
}
/**
* Creates a modifiable list with all of the items of the provided array in
* the same order. This method behaves much like {@code Arrays.asList},
* except that if the provided array is {@code null}, then it will return a
* {@code null} list rather than throwing an exception.
*
* @param The type of item contained in the provided array.
*
* @param array The array of items to include in the list.
*
* @return The list that was created, or {@code null} if the provided array
* was {@code null}.
*/
@Nullable()
public static List toList(@Nullable final T[] array)
{
if (array == null)
{
return null;
}
final ArrayList l = new ArrayList<>(array.length);
l.addAll(Arrays.asList(array));
return l;
}
/**
* Creates a modifiable list with all of the items of the provided array in
* the same order. This method behaves much like {@code Arrays.asList},
* except that if the provided array is {@code null}, then it will return an
* empty list rather than throwing an exception.
*
* @param The type of item contained in the provided array.
*
* @param array The array of items to include in the list.
*
* @return The list that was created, or an empty list if the provided array
* was {@code null}.
*/
@NotNull()
public static List toNonNullList(@Nullable final T[] array)
{
if (array == null)
{
return new ArrayList<>(0);
}
final ArrayList l = new ArrayList<>(array.length);
l.addAll(Arrays.asList(array));
return l;
}
/**
* Indicates whether both of the provided objects are {@code null} or both
* are logically equal (using the {@code equals} method).
*
* @param o1 The first object for which to make the determination.
* @param o2 The second object for which to make the determination.
*
* @return {@code true} if both objects are {@code null} or both are
* logically equal, or {@code false} if only one of the objects is
* {@code null} or they are not logically equal.
*/
public static boolean bothNullOrEqual(@Nullable final Object o1,
@Nullable final Object o2)
{
if (o1 == null)
{
return (o2 == null);
}
else if (o2 == null)
{
return false;
}
return o1.equals(o2);
}
/**
* Indicates whether both of the provided strings are {@code null} or both
* are logically equal ignoring differences in capitalization (using the
* {@code equalsIgnoreCase} method).
*
* @param s1 The first string for which to make the determination.
* @param s2 The second string for which to make the determination.
*
* @return {@code true} if both strings are {@code null} or both are
* logically equal ignoring differences in capitalization, or
* {@code false} if only one of the objects is {@code null} or they
* are not logically equal ignoring capitalization.
*/
public static boolean bothNullOrEqualIgnoreCase(@Nullable final String s1,
@Nullable final String s2)
{
if (s1 == null)
{
return (s2 == null);
}
else if (s2 == null)
{
return false;
}
return s1.equalsIgnoreCase(s2);
}
/**
* Indicates whether the provided string arrays have the same elements,
* ignoring the order in which they appear and differences in capitalization.
* It is assumed that neither array contains {@code null} strings, and that
* no string appears more than once in each array.
*
* @param a1 The first array for which to make the determination.
* @param a2 The second array for which to make the determination.
*
* @return {@code true} if both arrays have the same set of strings, or
* {@code false} if not.
*/
public static boolean stringsEqualIgnoreCaseOrderIndependent(
@Nullable final String[] a1,
@Nullable final String[] a2)
{
if (a1 == null)
{
return (a2 == null);
}
else if (a2 == null)
{
return false;
}
if (a1.length != a2.length)
{
return false;
}
if (a1.length == 1)
{
return (a1[0].equalsIgnoreCase(a2[0]));
}
final HashSet s1 = new HashSet<>(computeMapCapacity(a1.length));
for (final String s : a1)
{
s1.add(toLowerCase(s));
}
final HashSet s2 = new HashSet<>(computeMapCapacity(a2.length));
for (final String s : a2)
{
s2.add(toLowerCase(s));
}
return s1.equals(s2);
}
/**
* Indicates whether the provided arrays have the same elements, ignoring the
* order in which they appear. It is assumed that neither array contains
* {@code null} elements, and that no element appears more than once in each
* array.
*
* @param The type of element contained in the arrays.
*
* @param a1 The first array for which to make the determination.
* @param a2 The second array for which to make the determination.
*
* @return {@code true} if both arrays have the same set of elements, or
* {@code false} if not.
*/
public static boolean arraysEqualOrderIndependent(@Nullable final T[] a1,
@Nullable final T[] a2)
{
if (a1 == null)
{
return (a2 == null);
}
else if (a2 == null)
{
return false;
}
if (a1.length != a2.length)
{
return false;
}
if (a1.length == 1)
{
return (a1[0].equals(a2[0]));
}
final HashSet s1 = new HashSet<>(Arrays.asList(a1));
final HashSet s2 = new HashSet<>(Arrays.asList(a2));
return s1.equals(s2);
}
/**
* Determines the number of bytes in a UTF-8 character that starts with the
* given byte.
*
* @param b The byte for which to make the determination.
*
* @return The number of bytes in a UTF-8 character that starts with the
* given byte, or -1 if it does not appear to be a valid first byte
* for a UTF-8 character.
*/
public static int numBytesInUTF8CharacterWithFirstByte(final byte b)
{
if ((b & 0x7F) == b)
{
return 1;
}
else if ((b & 0xE0) == 0xC0)
{
return 2;
}
else if ((b & 0xF0) == 0xE0)
{
return 3;
}
else if ((b & 0xF8) == 0xF0)
{
return 4;
}
else
{
return -1;
}
}
/**
* Indicates whether the provided attribute name should be considered a
* sensitive attribute for the purposes of {@code toCode} methods. If an
* attribute is considered sensitive, then its values will be redacted in the
* output of the {@code toCode} methods.
*
* @param name The name for which to make the determination. It may or may
* not include attribute options. It must not be {@code null}.
*
* @return {@code true} if the specified attribute is one that should be
* considered sensitive for the
*/
public static boolean isSensitiveToCodeAttribute(@NotNull final String name)
{
final String lowerBaseName = Attribute.getBaseName(name).toLowerCase();
return TO_CODE_SENSITIVE_ATTRIBUTE_NAMES.contains(lowerBaseName);
}
/**
* Retrieves a set containing the base names (in all lowercase characters) of
* any attributes that should be considered sensitive for the purposes of the
* {@code toCode} methods. By default, only the userPassword and
* authPassword attributes and their respective OIDs will be included.
*
* @return A set containing the base names (in all lowercase characters) of
* any attributes that should be considered sensitive for the
* purposes of the {@code toCode} methods.
*/
@NotNull()
public static Set getSensitiveToCodeAttributeBaseNames()
{
return TO_CODE_SENSITIVE_ATTRIBUTE_NAMES;
}
/**
* Specifies the names of any attributes that should be considered sensitive
* for the purposes of the {@code toCode} methods.
*
* @param names The names of any attributes that should be considered
* sensitive for the purposes of the {@code toCode} methods.
* It may be {@code null} or empty if no attributes should be
* considered sensitive.
*/
public static void setSensitiveToCodeAttributes(
@Nullable final String... names)
{
setSensitiveToCodeAttributes(toList(names));
}
/**
* Specifies the names of any attributes that should be considered sensitive
* for the purposes of the {@code toCode} methods.
*
* @param names The names of any attributes that should be considered
* sensitive for the purposes of the {@code toCode} methods.
* It may be {@code null} or empty if no attributes should be
* considered sensitive.
*/
public static void setSensitiveToCodeAttributes(
@Nullable final Collection names)
{
if ((names == null) || names.isEmpty())
{
TO_CODE_SENSITIVE_ATTRIBUTE_NAMES = Collections.emptySet();
}
else
{
final LinkedHashSet nameSet = new LinkedHashSet<>(names.size());
for (final String s : names)
{
nameSet.add(Attribute.getBaseName(s).toLowerCase());
}
TO_CODE_SENSITIVE_ATTRIBUTE_NAMES = Collections.unmodifiableSet(nameSet);
}
}
/**
* Creates a new {@code IOException} with a cause. The constructor needed to
* do this wasn't available until Java SE 6, so reflection is used to invoke
* this constructor in versions of Java that provide it. In Java SE 5, the
* provided message will be augmented with information about the cause.
*
* @param message The message to use for the exception. This may be
* {@code null} if the message should be generated from the
* provided cause.
* @param cause The underlying cause for the exception. It may be
* {@code null} if the exception should have only a message.
*
* @return The {@code IOException} object that was created.
*/
@NotNull()
public static IOException createIOExceptionWithCause(
@Nullable final String message,
@Nullable final Throwable cause)
{
if (cause == null)
{
return new IOException(message);
}
else if (message == null)
{
return new IOException(cause);
}
else
{
return new IOException(message, cause);
}
}
/**
* Converts the provided string (which may include line breaks) into a list
* containing the lines without the line breaks.
*
* @param s The string to convert into a list of its representative lines.
*
* @return A list containing the lines that comprise the given string.
*/
@NotNull()
public static List stringToLines(@Nullable final String s)
{
final ArrayList l = new ArrayList<>(10);
if (s == null)
{
return l;
}
final BufferedReader reader = new BufferedReader(new StringReader(s));
try
{
while (true)
{
try
{
final String line = reader.readLine();
if (line == null)
{
return l;
}
else
{
l.add(line);
}
}
catch (final Exception e)
{
Debug.debugException(e);
// This should never happen. If it does, just return a list
// containing a single item that is the original string.
l.clear();
l.add(s);
return l;
}
}
}
finally
{
try
{
// This is technically not necessary in this case, but it's good form.
reader.close();
}
catch (final Exception e)
{
Debug.debugException(e);
// This should never happen, and there's nothing we need to do even if
// it does.
}
}
}
/**
* Creates a string that is a concatenation of all of the provided lines, with
* a line break (using the end-of-line sequence appropriate for the underlying
* platform) after each line (including the last line).
*
* @param lines The lines to include in the string.
*
* @return The string resulting from concatenating the provided lines with
* line breaks.
*/
@NotNull()
public static String linesToString(@Nullable final CharSequence... lines)
{
if (lines == null)
{
return "";
}
return linesToString(Arrays.asList(lines));
}
/**
* Creates a string that is a concatenation of all of the provided lines, with
* a line break (using the end-of-line sequence appropriate for the underlying
* platform) after each line (including the last line).
*
* @param lines The lines to include in the string.
*
* @return The string resulting from concatenating the provided lines with
* line breaks.
*/
@NotNull()
public static String linesToString(
@Nullable final List extends CharSequence> lines)
{
if (lines == null)
{
return "";
}
final StringBuilder buffer = new StringBuilder();
for (final CharSequence line : lines)
{
buffer.append(line);
buffer.append(EOL);
}
return buffer.toString();
}
/**
* Constructs a {@code File} object from the provided path.
*
* @param baseDirectory The base directory to use as the starting point.
* It must not be {@code null} and is expected to
* represent a directory.
* @param pathElements An array of the elements that make up the remainder
* of the path to the specified file, in order from
* paths closest to the root of the filesystem to
* furthest away (that is, the first element should
* represent a file or directory immediately below the
* base directory, the second is one level below that,
* and so on). It may be {@code null} or empty if the
* base directory should be used.
*
* @return The constructed {@code File} object.
*/
@NotNull()
public static File constructPath(@NotNull final File baseDirectory,
@Nullable final String... pathElements)
{
Validator.ensureNotNull(baseDirectory);
File f = baseDirectory;
if (pathElements != null)
{
for (final String pathElement : pathElements)
{
f = new File(f, pathElement);
}
}
return f;
}
/**
* Creates a byte array from the provided integer values. All of the integer
* values must be between 0x00 and 0xFF (0 and 255), inclusive. Any bits
* set outside of that range will be ignored.
*
* @param bytes The values to include in the byte array.
*
* @return A byte array with the provided set of values.
*/
@NotNull()
public static byte[] byteArray(@Nullable final int... bytes)
{
if ((bytes == null) || (bytes.length == 0))
{
return NO_BYTES;
}
final byte[] byteArray = new byte[bytes.length];
for (int i=0; i < bytes.length; i++)
{
byteArray[i] = (byte) (bytes[i] & 0xFF);
}
return byteArray;
}
/**
* Indicates whether the unit tests are currently running in this JVM.
*
* @return {@code true} if the unit tests are currently running, or
* {@code false} if not.
*/
public static boolean isWithinUnitTest()
{
return IS_WITHIN_UNIT_TESTS;
}
/**
* Throws an {@code Error} or a {@code RuntimeException} based on the provided
* {@code Throwable} object. This method will always throw something,
* regardless of the provided {@code Throwable} object.
*
* @param throwable The {@code Throwable} object to use to create the
* exception to throw.
*
* @throws Error If the provided {@code Throwable} object is an
* {@code Error} instance, then that {@code Error} instance
* will be re-thrown.
*
* @throws RuntimeException If the provided {@code Throwable} object is a
* {@code RuntimeException} instance, then that
* {@code RuntimeException} instance will be
* re-thrown. Otherwise, it must be a checked
* exception and that checked exception will be
* re-thrown as a {@code RuntimeException}.
*/
public static void throwErrorOrRuntimeException(
@NotNull final Throwable throwable)
throws Error, RuntimeException
{
Validator.ensureNotNull(throwable);
if (throwable instanceof Error)
{
throw (Error) throwable;
}
else if (throwable instanceof RuntimeException)
{
throw (RuntimeException) throwable;
}
else
{
throw new RuntimeException(throwable);
}
}
/**
* Re-throws the provided {@code Throwable} instance only if it is an
* {@code Error} or a {@code RuntimeException} instance; otherwise, this
* method will return without taking any action.
*
* @param throwable The {@code Throwable} object to examine and potentially
* re-throw.
*
* @throws Error If the provided {@code Throwable} object is an
* {@code Error} instance, then that {@code Error} instance
* will be re-thrown.
*
* @throws RuntimeException If the provided {@code Throwable} object is a
* {@code RuntimeException} instance, then that
* {@code RuntimeException} instance will be
* re-thrown.
*/
public static void rethrowIfErrorOrRuntimeException(
@NotNull final Throwable throwable)
throws Error, RuntimeException
{
if (throwable instanceof Error)
{
throw (Error) throwable;
}
else if (throwable instanceof RuntimeException)
{
throw (RuntimeException) throwable;
}
}
/**
* Re-throws the provided {@code Throwable} instance only if it is an
* {@code Error}; otherwise, this method will return without taking any
* action.
*
* @param throwable The {@code Throwable} object to examine and potentially
* re-throw.
*
* @throws Error If the provided {@code Throwable} object is an
* {@code Error} instance, then that {@code Error} instance
* will be re-thrown.
*/
public static void rethrowIfError(@NotNull final Throwable throwable)
throws Error
{
if (throwable instanceof Error)
{
throw (Error) throwable;
}
}
/**
* Computes the capacity that should be used for a map or a set with the
* expected number of elements, which can help avoid the need to re-hash or
* re-balance the map if too many items are added. This method bases its
* computation on the default map load factor of 0.75.
*
* @param expectedItemCount The expected maximum number of items that will
* be placed in the map or set. It must be greater
* than or equal to zero.
*
* @return The capacity that should be used for a map or a set with the
* expected number of elements
*/
public static int computeMapCapacity(final int expectedItemCount)
{
switch (expectedItemCount)
{
case 0:
return 0;
case 1:
return 2;
case 2:
return 3;
case 3:
return 5;
case 4:
return 6;
case 5:
return 7;
case 6:
return 9;
case 7:
return 10;
case 8:
return 11;
case 9:
return 13;
case 10:
return 14;
case 11:
return 15;
case 12:
return 17;
case 13:
return 18;
case 14:
return 19;
case 15:
return 21;
case 16:
return 22;
case 17:
return 23;
case 18:
return 25;
case 19:
return 26;
case 20:
return 27;
case 30:
return 41;
case 40:
return 54;
case 50:
return 67;
case 60:
return 81;
case 70:
return 94;
case 80:
return 107;
case 90:
return 121;
case 100:
return 134;
case 110:
return 147;
case 120:
return 161;
case 130:
return 174;
case 140:
return 187;
case 150:
return 201;
case 160:
return 214;
case 170:
return 227;
case 180:
return 241;
case 190:
return 254;
case 200:
return 267;
default:
Validator.ensureTrue((expectedItemCount >= 0),
"StaticUtils.computeMapOrSetCapacity.expectedItemCount must be " +
"greater than or equal to zero.");
// NOTE: 536,870,911 is Integer.MAX_VALUE/4. If the value is larger
// than that, then we'll fall back to using floating-point arithmetic
//
if (expectedItemCount > 536_870_911)
{
final int computedCapacity = ((int) (expectedItemCount / 0.75)) + 1;
if (computedCapacity <= expectedItemCount)
{
// This suggests that the expected number of items is so big that
// the computed capacity can't be adequately represented by an
// integer. In that case, we'll just return the expected item
// count and let the map or set get re-hashed/re-balanced if it
// actually gets anywhere near that size.
return expectedItemCount;
}
else
{
return computedCapacity;
}
}
else
{
return ((expectedItemCount * 4) / 3) + 1;
}
}
}
/**
* Creates an unmodifiable set containing the provided items. The iteration
* order of the provided items will be preserved.
*
* @param The type of item to include in the set.
* @param items The items to include in the set. It must not be
* {@code null}, but may be empty.
*
* @return An unmodifiable set containing the provided items.
*/
@SafeVarargs()
@SuppressWarnings("varargs")
@NotNull()
public static Set setOf(@NotNull final T... items)
{
return Collections.unmodifiableSet(
new LinkedHashSet<>(Arrays.asList(items)));
}
/**
* Creates a {@code HashSet} containing the provided items.
*
* @param The type of item to include in the set.
* @param items The items to include in the set. It must not be
* {@code null}, but may be empty.
*
* @return A {@code HashSet} containing the provided items.
*/
@SafeVarargs()
@SuppressWarnings("varargs")
@NotNull()
public static HashSet hashSetOf(@NotNull final T... items)
{
return new HashSet<>(Arrays.asList(items));
}
/**
* Creates a {@code LinkedHashSet} containing the provided items.
*
* @param The type of item to include in the set.
* @param items The items to include in the set. It must not be
* {@code null}, but may be empty.
*
* @return A {@code LinkedHashSet} containing the provided items.
*/
@SafeVarargs()
@SuppressWarnings("varargs")
@NotNull()
public static LinkedHashSet linkedHashSetOf(@NotNull final T... items)
{
return new LinkedHashSet<>(Arrays.asList(items));
}
/**
* Creates a {@code TreeSet} containing the provided items.
*
* @param The type of item to include in the set.
* @param items The items to include in the set. It must not be
* {@code null}, but may be empty.
*
* @return A {@code LinkedHashSet} containing the provided items.
*/
@SafeVarargs()
@SuppressWarnings("varargs")
@NotNull()
public static TreeSet treeSetOf(@NotNull final T... items)
{
return new TreeSet<>(Arrays.asList(items));
}
/**
* Creates an unmodifiable map containing the provided items.
*
* @param The type for the map keys.
* @param The type for the map values.
* @param key The only key to include in the map.
* @param value The only value to include in the map.
*
* @return The unmodifiable map that was created.
*/
@NotNull()
public static Map mapOf(@NotNull final K key,
@NotNull final V value)
{
return Collections.singletonMap(key, value);
}
/**
* Creates an unmodifiable map containing the provided items.
*
* @param The type for the map keys.
* @param The type for the map values.
* @param key1 The first key to include in the map.
* @param value1 The first value to include in the map.
* @param key2 The second key to include in the map.
* @param value2 The second value to include in the map.
*
* @return The unmodifiable map that was created.
*/
@NotNull()
public static Map mapOf(@NotNull final K key1,
@NotNull final V value1,
@NotNull final K key2,
@NotNull final V value2)
{
final LinkedHashMap map = new LinkedHashMap<>(computeMapCapacity(2));
map.put(key1, value1);
map.put(key2, value2);
return Collections.unmodifiableMap(map);
}
/**
* Creates an unmodifiable map containing the provided items.
*
* @param The type for the map keys.
* @param The type for the map values.
* @param key1 The first key to include in the map.
* @param value1 The first value to include in the map.
* @param key2 The second key to include in the map.
* @param value2 The second value to include in the map.
* @param key3 The third key to include in the map.
* @param value3 The third value to include in the map.
*
* @return The unmodifiable map that was created.
*/
@NotNull()
public static Map mapOf(@NotNull final K key1,
@NotNull final V value1,
@NotNull final K key2,
@NotNull final V value2,
@NotNull final K key3,
@NotNull final V value3)
{
final LinkedHashMap map = new LinkedHashMap<>(computeMapCapacity(3));
map.put(key1, value1);
map.put(key2, value2);
map.put(key3, value3);
return Collections.unmodifiableMap(map);
}
/**
* Creates an unmodifiable map containing the provided items.
*
* @param The type for the map keys.
* @param The type for the map values.
* @param key1 The first key to include in the map.
* @param value1 The first value to include in the map.
* @param key2 The second key to include in the map.
* @param value2 The second value to include in the map.
* @param key3 The third key to include in the map.
* @param value3 The third value to include in the map.
* @param key4 The fourth key to include in the map.
* @param value4 The fourth value to include in the map.
*
* @return The unmodifiable map that was created.
*/
@NotNull()
public static Map mapOf(@NotNull final K key1,
@NotNull final V value1,
@NotNull final K key2,
@NotNull final V value2,
@NotNull final K key3,
@NotNull final V value3,
@NotNull final K key4,
@NotNull final V value4)
{
final LinkedHashMap map = new LinkedHashMap<>(computeMapCapacity(4));
map.put(key1, value1);
map.put(key2, value2);
map.put(key3, value3);
map.put(key4, value4);
return Collections.unmodifiableMap(map);
}
/**
* Creates an unmodifiable map containing the provided items.
*
* @param The type for the map keys.
* @param The type for the map values.
* @param key1 The first key to include in the map.
* @param value1 The first value to include in the map.
* @param key2 The second key to include in the map.
* @param value2 The second value to include in the map.
* @param key3 The third key to include in the map.
* @param value3 The third value to include in the map.
* @param key4 The fourth key to include in the map.
* @param value4 The fourth value to include in the map.
* @param key5 The fifth key to include in the map.
* @param value5 The fifth value to include in the map.
*
* @return The unmodifiable map that was created.
*/
@NotNull()
public static Map mapOf(@NotNull final K key1,
@NotNull final V value1,
@NotNull final K key2,
@NotNull final V value2,
@NotNull final K key3,
@NotNull final V value3,
@NotNull final K key4,
@NotNull final V value4,
@NotNull final K key5,
@NotNull final V value5)
{
final LinkedHashMap map = new LinkedHashMap<>(computeMapCapacity(5));
map.put(key1, value1);
map.put(key2, value2);
map.put(key3, value3);
map.put(key4, value4);
map.put(key5, value5);
return Collections.unmodifiableMap(map);
}
/**
* Creates an unmodifiable map containing the provided items.
*
* @param The type for the map keys.
* @param The type for the map values.
* @param key1 The first key to include in the map.
* @param value1 The first value to include in the map.
* @param key2 The second key to include in the map.
* @param value2 The second value to include in the map.
* @param key3 The third key to include in the map.
* @param value3 The third value to include in the map.
* @param key4 The fourth key to include in the map.
* @param value4 The fourth value to include in the map.
* @param key5 The fifth key to include in the map.
* @param value5 The fifth value to include in the map.
* @param key6 The sixth key to include in the map.
* @param value6 The sixth value to include in the map.
*
* @return The unmodifiable map that was created.
*/
@NotNull()
public static Map mapOf(@NotNull final K key1,
@NotNull final V value1,
@NotNull final K key2,
@NotNull final V value2,
@NotNull final K key3,
@NotNull final V value3,
@NotNull final K key4,
@NotNull final V value4,
@NotNull final K key5,
@NotNull final V value5,
@NotNull final K key6,
@NotNull final V value6)
{
final LinkedHashMap map = new LinkedHashMap<>(computeMapCapacity(6));
map.put(key1, value1);
map.put(key2, value2);
map.put(key3, value3);
map.put(key4, value4);
map.put(key5, value5);
map.put(key6, value6);
return Collections.unmodifiableMap(map);
}
/**
* Creates an unmodifiable map containing the provided items.
*
* @param The type for the map keys.
* @param The type for the map values.
* @param key1 The first key to include in the map.
* @param value1 The first value to include in the map.
* @param key2 The second key to include in the map.
* @param value2 The second value to include in the map.
* @param key3 The third key to include in the map.
* @param value3 The third value to include in the map.
* @param key4 The fourth key to include in the map.
* @param value4 The fourth value to include in the map.
* @param key5 The fifth key to include in the map.
* @param value5 The fifth value to include in the map.
* @param key6 The sixth key to include in the map.
* @param value6 The sixth value to include in the map.
* @param key7 The seventh key to include in the map.
* @param value7 The seventh value to include in the map.
*
* @return The unmodifiable map that was created.
*/
@NotNull()
public static Map mapOf(@NotNull final K key1,
@NotNull final V value1,
@NotNull final K key2,
@NotNull final V value2,
@NotNull final K key3,
@NotNull final V value3,
@NotNull final K key4,
@NotNull final V value4,
@NotNull final K key5,
@NotNull final V value5,
@NotNull final K key6,
@NotNull final V value6,
@NotNull final K key7,
@NotNull final V value7)
{
final LinkedHashMap map = new LinkedHashMap<>(computeMapCapacity(7));
map.put(key1, value1);
map.put(key2, value2);
map.put(key3, value3);
map.put(key4, value4);
map.put(key5, value5);
map.put(key6, value6);
map.put(key7, value7);
return Collections.unmodifiableMap(map);
}
/**
* Creates an unmodifiable map containing the provided items.
*
* @param The type for the map keys.
* @param The type for the map values.
* @param key1 The first key to include in the map.
* @param value1 The first value to include in the map.
* @param key2 The second key to include in the map.
* @param value2 The second value to include in the map.
* @param key3 The third key to include in the map.
* @param value3 The third value to include in the map.
* @param key4 The fourth key to include in the map.
* @param value4 The fourth value to include in the map.
* @param key5 The fifth key to include in the map.
* @param value5 The fifth value to include in the map.
* @param key6 The sixth key to include in the map.
* @param value6 The sixth value to include in the map.
* @param key7 The seventh key to include in the map.
* @param value7 The seventh value to include in the map.
* @param key8 The eighth key to include in the map.
* @param value8 The eighth value to include in the map.
*
* @return The unmodifiable map that was created.
*/
@NotNull()
public static Map mapOf(@NotNull final K key1,
@NotNull final V value1,
@NotNull final K key2,
@NotNull final V value2,
@NotNull final K key3,
@NotNull final V value3,
@NotNull final K key4,
@NotNull final V value4,
@NotNull final K key5,
@NotNull final V value5,
@NotNull final K key6,
@NotNull final V value6,
@NotNull final K key7,
@NotNull final V value7,
@NotNull final K key8,
@NotNull final V value8)
{
final LinkedHashMap map = new LinkedHashMap<>(computeMapCapacity(8));
map.put(key1, value1);
map.put(key2, value2);
map.put(key3, value3);
map.put(key4, value4);
map.put(key5, value5);
map.put(key6, value6);
map.put(key7, value7);
map.put(key8, value8);
return Collections.unmodifiableMap(map);
}
/**
* Creates an unmodifiable map containing the provided items.
*
* @param The type for the map keys.
* @param The type for the map values.
* @param key1 The first key to include in the map.
* @param value1 The first value to include in the map.
* @param key2 The second key to include in the map.
* @param value2 The second value to include in the map.
* @param key3 The third key to include in the map.
* @param value3 The third value to include in the map.
* @param key4 The fourth key to include in the map.
* @param value4 The fourth value to include in the map.
* @param key5 The fifth key to include in the map.
* @param value5 The fifth value to include in the map.
* @param key6 The sixth key to include in the map.
* @param value6 The sixth value to include in the map.
* @param key7 The seventh key to include in the map.
* @param value7 The seventh value to include in the map.
* @param key8 The eighth key to include in the map.
* @param value8 The eighth value to include in the map.
* @param key9 The ninth key to include in the map.
* @param value9 The ninth value to include in the map.
*
* @return The unmodifiable map that was created.
*/
@NotNull()
public static Map mapOf(@NotNull final K key1,
@NotNull final V value1,
@NotNull final K key2,
@NotNull final V value2,
@NotNull final K key3,
@NotNull final V value3,
@NotNull final K key4,
@NotNull final V value4,
@NotNull final K key5,
@NotNull final V value5,
@NotNull final K key6,
@NotNull final V value6,
@NotNull final K key7,
@NotNull final V value7,
@NotNull final K key8,
@NotNull final V value8,
@NotNull final K key9,
@NotNull final V value9)
{
final LinkedHashMap map = new LinkedHashMap<>(computeMapCapacity(9));
map.put(key1, value1);
map.put(key2, value2);
map.put(key3, value3);
map.put(key4, value4);
map.put(key5, value5);
map.put(key6, value6);
map.put(key7, value7);
map.put(key8, value8);
map.put(key9, value9);
return Collections.unmodifiableMap(map);
}
/**
* Creates an unmodifiable map containing the provided items.
*
* @param The type for the map keys.
* @param The type for the map values.
* @param key1 The first key to include in the map.
* @param value1 The first value to include in the map.
* @param key2 The second key to include in the map.
* @param value2 The second value to include in the map.
* @param key3 The third key to include in the map.
* @param value3 The third value to include in the map.
* @param key4 The fourth key to include in the map.
* @param value4 The fourth value to include in the map.
* @param key5 The fifth key to include in the map.
* @param value5 The fifth value to include in the map.
* @param key6 The sixth key to include in the map.
* @param value6 The sixth value to include in the map.
* @param key7 The seventh key to include in the map.
* @param value7 The seventh value to include in the map.
* @param key8 The eighth key to include in the map.
* @param value8 The eighth value to include in the map.
* @param key9 The ninth key to include in the map.
* @param value9 The ninth value to include in the map.
* @param key10 The tenth key to include in the map.
* @param value10 The tenth value to include in the map.
*
* @return The unmodifiable map that was created.
*/
@NotNull()
public static Map mapOf(@NotNull final K key1,
@NotNull final V value1,
@NotNull final K key2,
@NotNull final V value2,
@NotNull final K key3,
@NotNull final V value3,
@NotNull final K key4,
@NotNull final V value4,
@NotNull final K key5,
@NotNull final V value5,
@NotNull final K key6,
@NotNull final V value6,
@NotNull final K key7,
@NotNull final V value7,
@NotNull final K key8,
@NotNull final V value8,
@NotNull final K key9,
@NotNull final V value9,
@NotNull final K key10,
@NotNull final V value10)
{
final LinkedHashMap map = new LinkedHashMap<>(computeMapCapacity(10));
map.put(key1, value1);
map.put(key2, value2);
map.put(key3, value3);
map.put(key4, value4);
map.put(key5, value5);
map.put(key6, value6);
map.put(key7, value7);
map.put(key8, value8);
map.put(key9, value9);
map.put(key10, value10);
return Collections.unmodifiableMap(map);
}
/**
* Creates an unmodifiable map containing the provided items. The map entries
* must have the same data type for keys and values.
*
* @param The type for the map keys and values.
* @param items The items to include in the map. If it is null or empty,
* the map will be empty. If it is non-empty, then the number
* of elements in the array must be a multiple of two.
* Elements in even-numbered indexes will be the keys for the
* map entries, while elements in odd-numbered indexes will be
* the map values.
*
* @return The unmodifiable map that was created.
*/
@SafeVarargs()
@NotNull()
public static Map mapOf(@Nullable final T... items)
{
if ((items == null) || (items.length == 0))
{
return Collections.emptyMap();
}
Validator.ensureTrue(((items.length % 2) == 0),
"StaticUtils.mapOf.items must have an even number of elements");
final int numEntries = items.length / 2;
final LinkedHashMap map =
new LinkedHashMap<>(computeMapCapacity(numEntries));
for (int i=0; i < items.length; )
{
map.put(items[i++], items[i++]);
}
return Collections.unmodifiableMap(map);
}
/**
* Creates an unmodifiable map containing the provided items.
*
* @param The type for the map keys.
* @param The type for the map values.
* @param items The items to include in the map.
*
* @return The unmodifiable map that was created.
*/
@SafeVarargs()
@NotNull()
public static Map mapOfObjectPairs(
@Nullable final ObjectPair... items)
{
if ((items == null) || (items.length == 0))
{
return Collections.emptyMap();
}
final LinkedHashMap map = new LinkedHashMap<>(
computeMapCapacity(items.length));
for (final ObjectPair item : items)
{
map.put(item.getFirst(), item.getSecond());
}
return Collections.unmodifiableMap(map);
}
/**
* Attempts to determine all addresses associated with the local system,
* including loopback addresses.
*
* @param nameResolver The name resolver to use to determine the local host
* and loopback addresses. If this is {@code null},
* then the LDAP SDK's default name resolver will be
* used.
*
* @return A set of the local addresses that were identified.
*/
@NotNull()
public static Set getAllLocalAddresses(
@Nullable final NameResolver nameResolver)
{
return getAllLocalAddresses(nameResolver, true);
}
/**
* Attempts to determine all addresses associated with the local system,
* optionally including loopback addresses.
*
* @param nameResolver The name resolver to use to determine the local
* host and loopback addresses. If this is
* {@code null}, then the LDAP SDK's default name
* resolver will be used.
* @param includeLoopback Indicates whether to include loopback addresses in
* the set that is returned.
*
* @return A set of the local addresses that were identified.
*/
@NotNull()
public static Set getAllLocalAddresses(
@Nullable final NameResolver nameResolver,
final boolean includeLoopback)
{
final NameResolver resolver;
if (nameResolver == null)
{
resolver = LDAPConnectionOptions.DEFAULT_NAME_RESOLVER;
}
else
{
resolver = nameResolver;
}
final LinkedHashSet localAddresses =
new LinkedHashSet<>(computeMapCapacity(10));
try
{
final InetAddress localHostAddress = resolver.getLocalHost();
if (includeLoopback || (! localHostAddress.isLoopbackAddress()))
{
localAddresses.add(localHostAddress);
}
}
catch (final Exception e)
{
Debug.debugException(e);
}
try
{
final Enumeration networkInterfaces =
NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements())
{
final NetworkInterface networkInterface =
networkInterfaces.nextElement();
if (includeLoopback || (! networkInterface.isLoopback()))
{
final Enumeration interfaceAddresses =
networkInterface.getInetAddresses();
while (interfaceAddresses.hasMoreElements())
{
final InetAddress address = interfaceAddresses.nextElement();
if (includeLoopback || (! address.isLoopbackAddress()))
{
localAddresses.add(address);
}
}
}
}
}
catch (final Exception e)
{
Debug.debugException(e);
}
if (includeLoopback)
{
try
{
localAddresses.add(resolver.getLoopbackAddress());
}
catch (final Exception e)
{
Debug.debugException(e);
}
}
return Collections.unmodifiableSet(localAddresses);
}
/**
* Retrieves the canonical host name for the provided address, if it can be
* resolved to a name.
*
* @param nameResolver The name resolver to use to obtain the canonical
* host name. If this is {@code null}, then the LDAP
* SDK's default name resolver will be used.
* @param address The {@code InetAddress} for which to attempt to
* obtain the canonical host name.
*
* @return The canonical host name for the provided address, or {@code null}
* if it cannot be obtained (either because the attempt returns
* {@code null}, which shouldn't happen, or because it matches the
* IP address).
*/
@Nullable()
public static String getCanonicalHostNameIfAvailable(
@Nullable final NameResolver nameResolver,
@NotNull final InetAddress address)
{
final NameResolver resolver;
if (nameResolver == null)
{
resolver = LDAPConnectionOptions.DEFAULT_NAME_RESOLVER;
}
else
{
resolver = nameResolver;
}
final String hostAddress = address.getHostAddress();
final String trimmedHostAddress =
trimInterfaceNameFromHostAddress(hostAddress);
final String canonicalHostName = resolver.getCanonicalHostName(address);
if ((canonicalHostName == null) ||
canonicalHostName.equalsIgnoreCase(hostAddress) ||
canonicalHostName.equalsIgnoreCase(trimmedHostAddress))
{
return null;
}
return canonicalHostName;
}
/**
* Retrieves the canonical host names for the provided set of
* {@code InetAddress} objects. If any of the provided addresses cannot be
* resolved to a canonical host name (in which case the attempt to get the
* canonical host name will return its IP address), it will be excluded from
* the returned set.
*
* @param nameResolver The name resolver to use to obtain the canonical
* host names. If this is {@code null}, then the LDAP
* SDK's default name resolver will be used.
* @param addresses The set of addresses for which to obtain the
* canonical host names.
*
* @return A set of the canonical host names that could be obtained from the
* provided addresses.
*/
@NotNull()
public static Set getAvailableCanonicalHostNames(
@Nullable final NameResolver nameResolver,
@NotNull final Collection addresses)
{
final NameResolver resolver;
if (nameResolver == null)
{
resolver = LDAPConnectionOptions.DEFAULT_NAME_RESOLVER;
}
else
{
resolver = nameResolver;
}
final Set canonicalHostNames =
new LinkedHashSet<>(computeMapCapacity(addresses.size()));
for (final InetAddress address : addresses)
{
final String canonicalHostName =
getCanonicalHostNameIfAvailable(resolver, address);
if (canonicalHostName != null)
{
canonicalHostNames.add(canonicalHostName);
}
}
return Collections.unmodifiableSet(canonicalHostNames);
}
/**
* Retrieves a version of the provided host address with the interface name
* stripped off. Java sometimes follows an IP address with a percent sign and
* the interface name. If that interface name is present in the provided
* host address, then this method will trim it off, leaving just the IP
* address. If the provided host address does not include the interface name,
* then the provided address will be returned as-is.
*
* @param hostAddress The host address to be trimmed.
*
* @return The provided host address without the interface name.
*/
@NotNull()
public static String trimInterfaceNameFromHostAddress(
@NotNull final String hostAddress)
{
final int percentPos = hostAddress.indexOf('%');
if (percentPos > 0)
{
return hostAddress.substring(0, percentPos);
}
else
{
return hostAddress;
}
}
/**
* Indicates whether the provided address is marked as reserved in the IANA
* IPv4 address space registry at
* https://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.txt
* or the IPv6 address space registry at
* https://www.iana.org/assignments/ipv6-address-space/ipv6-address-space.txt.
*
* @param address
* The address for which to make the determination. It must
* not be {@code null}, and it must be an IPv4 or IPv6 address.
* @param includePrivateUseNetworkAddresses
* Indicates whether to consider addresses in a private-use
* network address range (including 10.0.0.0/8, 172.16.0.0/12,
* 192.168.0.0/16, and fc00::/7) as reserved addresses. If this
* is {@code true}, then this method will return {@code true} for
* addresses in a private-use network range; if it is
* {@code false}, then this method will return {@code false} for
* addresses in those ranges. This does not have any effect for
* addresses in other reserved address ranges.
*
* @return {@code true} if the provided address is in a reserved address
* range, or {@code false} if not.
*/
public static boolean isIANAReservedIPAddress(
@NotNull final InetAddress address,
final boolean includePrivateUseNetworkAddresses)
{
if (address instanceof Inet4Address)
{
return isIANAReservedIPv4Address((Inet4Address) address,
includePrivateUseNetworkAddresses);
}
else if (address instanceof Inet6Address)
{
return isIANAReservedIPv6Address((Inet6Address) address,
includePrivateUseNetworkAddresses);
}
else
{
// It's an unrecognized address type. We have to assume it's not
// reserved.
return false;
}
}
/**
* Indicates whether the provided address is marked as reserved in the IANA
* IPv4 address space registry at
* https://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.txt.
* This implementation is based on the version of the registry that was
* updated on 2019-12-27.
*
* @param address
* The IPv4 address for which to make the determination. It must
* not be {@code null}, and it must be an IPv4 address.
* @param includePrivateUseNetworkAddresses
* Indicates whether to consider addresses in a private-use
* network address range as reserved addresses.
*
* @return {@code true} if the provided address is in a reserved address
* range, or {@code false} if not.
*/
public static boolean isIANAReservedIPv4Address(
@NotNull final Inet4Address address,
final boolean includePrivateUseNetworkAddresses)
{
final byte[] addressBytes = address.getAddress();
final int firstOctet = addressBytes[0] & 0xFF;
final int secondOctet = addressBytes[1] & 0xFF;
final int thirdOctet = addressBytes[2] & 0xFF;
switch (firstOctet)
{
// * Addresses 0.*.*.* are reserved for self-identification.
case 0:
// * Addresses 127.*.*.* are reserved for loopback addresses.
case 127:
// * Addresses 224.*.*.* through 239.*.*.* are reserved for multicast.
case 224:
case 225:
case 226:
case 227:
case 228:
case 229:
case 230:
case 231:
case 232:
case 233:
case 234:
case 235:
case 236:
case 237:
case 238:
case 239:
// * Addresses 240.*.*.* through 255.*.*.* are reserved for future use.
case 240:
case 241:
case 242:
case 243:
case 244:
case 245:
case 246:
case 247:
case 248:
case 249:
case 250:
case 251:
case 252:
case 253:
case 254:
case 255:
return true;
// * Addresses 10.*.*.* are reserved for private-use networks.
case 10:
return includePrivateUseNetworkAddresses;
// * Addresses 100.64.0.0 through 100.127.255.255. are in the shared
// address space range described in RFC 6598.
case 100: // First octet 100 -- Partially reserved
return ((secondOctet >= 64) && (secondOctet <= 127));
// * Addresses 169.254.*.* are reserved for link-local addresses.
case 169:
return (secondOctet == 254);
// * Addresses 172.16.0.0 through 172.31.255.255 are reserved for
// private-use networks.
case 172:
if ((secondOctet >= 16) && (secondOctet <= 31))
{
return includePrivateUseNetworkAddresses;
}
else
{
return false;
}
// * Addresses 192.0.0.* are reserved for IPv4 Special Purpose Address.
// * Addresses 192.0.2.* are reserved for TEST-NET-1.
// * Addresses 192.88.99.* are reserved for 6to4 Relay Anycast.
// * Addresses 192.168.*.* are reserved for private-use networks.
case 192:
if (secondOctet == 0)
{
return ((thirdOctet == 0) || (thirdOctet == 2));
}
else if (secondOctet == 88)
{
return (thirdOctet == 99);
}
else if (secondOctet == 168)
{
return includePrivateUseNetworkAddresses;
}
else
{
return false;
}
// * Addresses 198.18.0.0 through 198.19.255.255 are reserved for Network
// Interconnect Device Benchmark Testing.
// * Addresses 198.51.100.* are reserved for TEST-NET-2.
case 198:
if ((secondOctet >= 18) && (secondOctet <= 19))
{
return true;
}
else
{
return ((secondOctet == 51) && (thirdOctet == 100));
}
// * Addresses 203.0.113.* are reserved for TEST-NET-3.
case 203:
return ((secondOctet == 0) && (thirdOctet == 113));
// All other addresses are not reserved.
default:
return false;
}
}
/**
* Indicates whether the provided address is marked as reserved in the IANA
* IPv6 address space registry at
* https://www.iana.org/assignments/ipv6-address-space/ipv6-address-space.txt.
* This implementation is based on the version of the registry that was
* updated on 2019-09-13.
*
* @param address
* The IPv4 address for which to make the determination. It must
* not be {@code null}, and it must be an IPv6 address.
* @param includePrivateUseNetworkAddresses
* Indicates whether to consider addresses in a private-use
* network address range as reserved addresses.
*
* @return {@code true} if the provided address is in a reserved address
* range, or {@code false} if not.
*/
public static boolean isIANAReservedIPv6Address(
@NotNull final Inet6Address address,
final boolean includePrivateUseNetworkAddresses)
{
final byte[] addressBytes = address.getAddress();
final int firstOctet = addressBytes[0] & 0xFF;
// Addresses with a first octet between 0x20 and 0x3F are not reserved.
if ((firstOctet >= 0x20) && (firstOctet <= 0x3F))
{
return false;
}
// Addresses with a first octet between 0xFC and 0xFD are reserved for
// private-use networks.
if ((firstOctet >= 0xFC) && (firstOctet <= 0xFD))
{
return includePrivateUseNetworkAddresses;
}
// All other addresses are reserved.
return true;
}
/**
* Reads the bytes that comprise the specified file.
*
* @param path The path to the file to be read.
*
* @return The bytes that comprise the specified file.
*
* @throws IOException If a problem occurs while trying to read the file.
*/
@NotNull()
public static byte[] readFileBytes(@NotNull final String path)
throws IOException
{
return readFileBytes(new File(path));
}
/**
* Reads the bytes that comprise the specified file.
*
* @param file The file to be read.
*
* @return The bytes that comprise the specified file.
*
* @throws IOException If a problem occurs while trying to read the file.
*/
@NotNull()
public static byte[] readFileBytes(@NotNull final File file)
throws IOException
{
final ByteStringBuffer buffer = new ByteStringBuffer((int) file.length());
buffer.readFrom(file);
return buffer.toByteArray();
}
/**
* Reads the contents of the specified file as a string. All line breaks in
* the file will be preserved, with the possible exception of the one on the
* last line.
*
* @param path The path to the file to be read.
* @param includeFinalLineBreak Indicates whether the final line break (if
* there is one) should be preserved.
*
* @return The contents of the specified file as a string.
*
* @throws IOException If a problem occurs while trying to read the file.
*/
@NotNull()
public static String readFileAsString(@NotNull final String path,
final boolean includeFinalLineBreak)
throws IOException
{
return readFileAsString(new File(path), includeFinalLineBreak);
}
/**
* Reads the contents of the specified file as a string. All line breaks in
* the file will be preserved, with the possible exception of the one on the
* last line.
*
* @param file The file to be read.
* @param includeFinalLineBreak Indicates whether the final line break (if
* there is one) should be preserved.
*
* @return The contents of the specified file as a string.
*
* @throws IOException If a problem occurs while trying to read the file.
*/
@NotNull()
public static String readFileAsString(@NotNull final File file,
final boolean includeFinalLineBreak)
throws IOException
{
final ByteStringBuffer buffer = new ByteStringBuffer((int) file.length());
buffer.readFrom(file);
if (! includeFinalLineBreak)
{
if (buffer.endsWith(EOL_BYTES_CR_LF))
{
buffer.setLength(buffer.length() - EOL_BYTES_CR_LF.length);
}
else if (buffer.endsWith(EOL_BYTES_LF))
{
buffer.setLength(buffer.length() - EOL_BYTES_LF.length);
}
}
return buffer.toString();
}
/**
* Reads the lines that comprise the specified file.
*
* @param path The path to the file to be read.
*
* @return The lines that comprise the specified file.
*
* @throws IOException If a problem occurs while trying to read the file.
*/
@NotNull()
public static List readFileLines(@NotNull final String path)
throws IOException
{
return readFileLines(new File(path));
}
/**
* Reads the lines that comprise the specified file.
*
* @param file The file to be read.
*
* @return The lines that comprise the specified file.
*
* @throws IOException If a problem occurs while trying to read the file.
*/
@NotNull()
public static List readFileLines(@NotNull final File file)
throws IOException
{
try (FileReader fileReader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(fileReader))
{
final List lines = new ArrayList<>();
while (true)
{
final String line = bufferedReader.readLine();
if (line == null)
{
return Collections.unmodifiableList(lines);
}
lines.add(line);
}
}
}
/**
* Writes the provided bytes to the specified file. If the file already
* exists, it will be overwritten.
*
* @param path The path to the file to be written.
* @param bytes The bytes to be written to the specified file.
*
* @throws IOException If a problem is encountered while writing the file.
*/
public static void writeFile(@NotNull final String path,
@NotNull final byte[] bytes)
throws IOException
{
writeFile(new File(path), bytes);
}
/**
* Writes the provided bytes to the specified file. If the file already
* exists, it will be overwritten.
*
* @param file The file to be written.
* @param bytes The bytes to be written to the specified file.
*
* @throws IOException If a problem is encountered while writing the file.
*/
public static void writeFile(@NotNull final File file,
@NotNull final byte[] bytes)
throws IOException
{
try (FileOutputStream outputStream = new FileOutputStream(file))
{
outputStream.write(bytes);
}
}
/**
* Writes the provided lines to the specified file, with each followed by an
* appropriate end-of-line marker for the current platform. If the file
* already exists, it will be overwritten.
*
* @param path The path to the file to be written.
* @param lines The lines to be written to the specified file.
*
* @throws IOException If a problem is encountered while writing the file.
*/
public static void writeFile(@NotNull final String path,
@NotNull final CharSequence... lines)
throws IOException
{
writeFile(new File(path), lines);
}
/**
* Writes the provided lines to the specified file, with each followed by an
* appropriate end-of-line marker for the current platform. If the file
* already exists, it will be overwritten.
*
* @param file The file to be written.
* @param lines The lines to be written to the specified file.
*
* @throws IOException If a problem is encountered while writing the file.
*/
public static void writeFile(@NotNull final File file,
@NotNull final CharSequence... lines)
throws IOException
{
writeFile(file, toList(lines));
}
/**
* Writes the provided lines to the specified file, with each followed by an
* appropriate end-of-line marker for the current platform. If the file
* already exists, it will be overwritten.
*
* @param path The path to the file to be written.
* @param lines The lines to be written to the specified file.
*
* @throws IOException If a problem is encountered while writing the file.
*/
public static void writeFile(@NotNull final String path,
@Nullable final List extends CharSequence> lines)
throws IOException
{
writeFile(new File(path), lines);
}
/**
* Writes the provided lines to the specified file, with each followed by an
* appropriate end-of-line marker for the current platform. If the file
* already exists, it will be overwritten.
*
* @param file The file to be written.
* @param lines The lines to be written to the specified file.
*
* @throws IOException If a problem is encountered while writing the file.
*/
public static void writeFile(@NotNull final File file,
@Nullable final List extends CharSequence> lines)
throws IOException
{
try (PrintWriter writer = new PrintWriter(file))
{
if (lines != null)
{
for (final CharSequence line : lines)
{
writer.println(line);
}
}
}
}
/**
* Retrieves a byte array with the specified number of randomly selected
* bytes.
*
* @param numBytes The number of bytes of random data to retrieve. It must
* be greater than or equal to zero.
* @param secure Indicates whether to use a cryptographically secure
* random number generator.
*
* @return A byte array with the specified number of randomly selected
* bytes.
*/
@NotNull()
public static byte[] randomBytes(final int numBytes,
final boolean secure)
{
final byte[] byteArray = new byte[numBytes];
getThreadLocalRandom(secure).nextBytes(byteArray);
return byteArray;
}
/**
* Retrieves a randomly selected integer between the given upper and lower
* bounds.
*
* @param lowerBound The lowest value that may be selected at random. It
* must be less than or equal to the upper bound.
* @param upperBound The highest value that may be selected at random. It
* must be greater than or equal to the lower bound.
* @param secure Indicates whether to use a cryptographically secure
* random number generator.
*
* @return A randomly selected integer between the given upper and lower
* bounds.
*/
public static int randomInt(final int lowerBound, final int upperBound,
final boolean secure)
{
// Compute the span of values. We need to use a long for this, because it's
// possible that this could cause an integer overflow.
final long span = 1L + upperBound - lowerBound;
// Select a random long value between zero and that span.
final long randomLong = getThreadLocalRandom(secure).nextLong();
final long positiveLong = randomLong & 0x7F_FF_FF_FF_FF_FF_FF_FFL;
final long valueWithinSpan = positiveLong % span;
return (int) (lowerBound + valueWithinSpan);
}
/**
* Retrieves a string containing the specified number of randomly selected
* ASCII letters. It will contain only lowercase letters.
*
* @param length The number of letters to include in the string. It must be
* greater than or equal to zero.
* @param secure Indicates whether to use a cryptographically secure random
* number generator.
*
* @return The randomly generated alphabetic string.
*/
@NotNull()
public static String randomAlphabeticString(final int length,
final boolean secure)
{
return randomString(length, LOWERCASE_LETTERS, secure);
}
/**
* Retrieves a string containing the specified number of randomly selected
* ASCII numeric digits.
*
* @param length The number of digits to include in the string. It must be
* greater than or equal to zero.
* @param secure Indicates whether to use a cryptographically secure random
* number generator.
*
* @return The randomly generated numeric string.
*/
@NotNull()
public static String randomNumericString(final int length,
final boolean secure)
{
return randomString(length, NUMERIC_DIGITS, secure);
}
/**
* Retrieves a string containing the specified number of randomly selected
* ASCII alphanumeric characters. It may contain a mix of lowercase letters,
* uppercase letters, and numeric digits.
*
* @param length The number of characters to include in the string. It must
* be greater than or equal to zero.
* @param secure Indicates whether to use a cryptographically secure random
* number generator.
*
* @return The randomly generated alphanumeric string.
*/
@NotNull()
public static String randomAlphanumericString(final int length,
final boolean secure)
{
return randomString(length, ALPHANUMERIC_CHARACTERS, secure);
}
/**
* Retrieves a string containing the specified number of randomly selected
* characters from the given set.
*
* @param length The number of characters to include in the string.
* It must be greater than or equal to zero.
* @param allowedChars The set of characters that are allowed to be included
* in the string. It must not be {@code null} or
* empty.
* @param secure Indicates whether to use a cryptographically secure
* random number generator.
*
* @return The randomly generated string.
*/
@NotNull()
public static String randomString(final int length,
@NotNull final char[] allowedChars,
final boolean secure)
{
final StringBuilder buffer = new StringBuilder(length);
final Random random = getThreadLocalRandom(secure);
for (int i=0; i < length; i++)
{
buffer.append(allowedChars[random.nextInt(allowedChars.length)]);
}
return buffer.toString();
}
/**
* Retrieves a thread-local random number generator.
*
* @param secure Indicates whether to retrieve a cryptographically secure
* random number generator.
*
* @return The thread-local random number generator.
*/
@NotNull()
private static Random getThreadLocalRandom(final boolean secure)
{
if (secure)
{
return ThreadLocalSecureRandom.get();
}
else
{
return ThreadLocalRandom.get();
}
}
}