/* * Copyright 2013 Robert von Burg <eitch@eitchnet.ch> * * 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. */ package li.strolch.utils.helper; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.MessageFormat; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A helper class to perform different actions on {@link String}s * * @author Robert von Burg <eitch@eitchnet.ch> */ public class StringHelper { public static final String NEW_LINE = "\n"; //$NON-NLS-1$ public static final String EMPTY = ""; //$NON-NLS-1$ public static final String SPACE = " "; //$NON-NLS-1$ public static final String NULL = "null"; //$NON-NLS-1$ public static final String DASH = "-"; //$NON-NLS-1$ public static final String UNDERLINE = "_"; //$NON-NLS-1$ public static final String COMMA = ","; //$NON-NLS-1$ public static final String DOT = "."; //$NON-NLS-1$ public static final String SEMICOLON = ";"; //$NON-NLS-1$ public static final String COLON = ":"; //$NON-NLS-1$ private static final Logger logger = LoggerFactory.getLogger(StringHelper.class); /** * the semi-unique id which is incremented on every {@link #getUniqueId()}-method call */ private static long uniqueId = System.currentTimeMillis() - 1119953500000l; /** * Hex char table for fast calculating of hex values */ private static final byte[] HEX_CHAR_TABLE = { (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f' }; /** * Converts each byte of the given byte array to a HEX value and returns the concatenation of these values * * @param raw * the bytes to convert to String using numbers in hexadecimal * * @return the encoded string * * @throws RuntimeException */ public static String getHexString(byte[] raw) throws RuntimeException { try { byte[] hex = new byte[2 * raw.length]; int index = 0; for (byte b : raw) { int v = b & 0xFF; hex[index++] = HEX_CHAR_TABLE[v >>> 4]; hex[index++] = HEX_CHAR_TABLE[v & 0xF]; } return new String(hex, "ASCII"); //$NON-NLS-1$ } catch (UnsupportedEncodingException e) { String msg = MessageFormat.format("Something went wrong while converting to HEX: {0}", e.getMessage()); //$NON-NLS-1$ throw new RuntimeException(msg, e); } } /** * Returns a byte array of a given string by converting each character of the string to a number base 16 * * @param encoded * the string to convert to a byt string * * @return the encoded byte stream */ public static byte[] fromHexString(String encoded) { if ((encoded.length() % 2) != 0) throw new IllegalArgumentException("Input string must contain an even number of characters."); //$NON-NLS-1$ final byte result[] = new byte[encoded.length() / 2]; final char enc[] = encoded.toCharArray(); for (int i = 0; i < enc.length; i += 2) { StringBuilder curr = new StringBuilder(2); curr.append(enc[i]).append(enc[i + 1]); result[i / 2] = (byte) Integer.parseInt(curr.toString(), 16); } return result; } /** * Generates the MD5 Hash of a string and converts it to a HEX string * * @param string * the string to hash * * @return the hash or null, if an exception was thrown */ public static String hashMd5AsHex(String string) { return getHexString(hashMd5(string.getBytes())); } /** * Generates the MD5 Hash of a string. Use {@link StringHelper#getHexString(byte[])} to convert the byte array to a * Hex String which is printable * * @param string * the string to hash * * @return the hash or null, if an exception was thrown */ public static byte[] hashMd5(String string) { return hashMd5(string.getBytes()); } /** * Generates the MD5 Hash of a byte array Use {@link StringHelper#getHexString(byte[])} to convert the byte array to * a Hex String which is printable * * @param bytes * the bytes to hash * * @return the hash or null, if an exception was thrown */ public static byte[] hashMd5(byte[] bytes) { return hash("MD5", bytes); //$NON-NLS-1$ } /** * Generates the SHA1 Hash of a string and converts it to a HEX String * * @param string * the string to hash * * @return the hash or null, if an exception was thrown */ public static String hashSha1AsHex(String string) { return getHexString(hashSha1(string.getBytes())); } /** * Generates the SHA1 Hash of a string Use {@link StringHelper#getHexString(byte[])} to convert the byte array to a * Hex String which is printable * * @param string * the string to hash * * @return the hash or null, if an exception was thrown */ public static byte[] hashSha1(String string) { return hashSha1(string.getBytes()); } /** * Generates the SHA1 Hash of a byte array Use {@link StringHelper#getHexString(byte[])} to convert the byte array * to a Hex String which is printable * * @param bytes * the bytes to hash * * @return the hash or null, if an exception was thrown */ public static byte[] hashSha1(byte[] bytes) { return hash("SHA-1", bytes); //$NON-NLS-1$ } /** * Generates the SHA-256 Hash of a string and converts it to a HEX String * * @param string * the string to hash * * @return the hash or null, if an exception was thrown */ public static String hashSha256AsHex(String string) { return getHexString(hashSha256(string.getBytes())); } /** * Generates the SHA-256 Hash of a string Use {@link StringHelper#getHexString(byte[])} to convert the byte array to * a Hex String which is printable * * @param string * the string to hash * * @return the hash or null, if an exception was thrown */ public static byte[] hashSha256(String string) { return hashSha256(string.getBytes()); } /** * Generates the SHA1 Hash of a byte array Use {@link StringHelper#getHexString(byte[])} to convert the byte array * to a Hex String which is printable * * @param bytes * the bytes to hash * * @return the hash or null, if an exception was thrown */ public static byte[] hashSha256(byte[] bytes) { return hash("SHA-256", bytes); //$NON-NLS-1$ } /** * Returns the hash of an algorithm * * @param algorithm * the algorithm to use * @param string * the string to hash * * @return the hash or null, if an exception was thrown */ public static String hashAsHex(String algorithm, String string) { return getHexString(hash(algorithm, string)); } /** * Returns the hash of an algorithm * * @param algorithm * the algorithm to use * @param string * the string to hash * * @return the hash or null, if an exception was thrown */ public static byte[] hash(String algorithm, String string) { try { MessageDigest digest = MessageDigest.getInstance(algorithm); byte[] hashArray = digest.digest(string.getBytes()); return hashArray; } catch (NoSuchAlgorithmException e) { String msg = MessageFormat.format("Algorithm {0} does not exist!", algorithm); //$NON-NLS-1$ throw new RuntimeException(msg, e); } } /** * Returns the hash of an algorithm * * @param algorithm * the algorithm to use * @param bytes * the bytes to hash * * @return the hash or null, if an exception was thrown */ public static String hashAsHex(String algorithm, byte[] bytes) { return getHexString(hash(algorithm, bytes)); } /** * Returns the hash of an algorithm * * @param algorithm * the algorithm to use * @param bytes * the bytes to hash * * @return the hash or null, if an exception was thrown */ public static byte[] hash(String algorithm, byte[] bytes) { try { MessageDigest digest = MessageDigest.getInstance(algorithm); byte[] hashArray = digest.digest(bytes); return hashArray; } catch (NoSuchAlgorithmException e) { String msg = MessageFormat.format("Algorithm {0} does not exist!", algorithm); //$NON-NLS-1$ throw new RuntimeException(msg, e); } } /** * Normalizes the length of a String. Does not shorten it when it is too long, but lengthens it, depending on the * options set: adding the char at the beginning or appending it at the end * * @param value * string to normalize * @param length * length string must have * @param beginning * add at beginning of value * @param c * char to append when appending * @return the new string */ public static String normalizeLength(String value, int length, boolean beginning, char c) { return normalizeLength(value, length, beginning, false, c); } /** * Normalizes the length of a String. Shortens it when it is too long, giving out a logger warning, or lengthens it, * depending on the options set: appending the char at the beginning or the end * * @param value * string to normalize * @param length * length string must have * @param beginning * append at beginning of value * @param shorten * allow shortening of value * @param c * char to append when appending * @return the new string */ public static String normalizeLength(String value, int length, boolean beginning, boolean shorten, char c) { if (value.length() == length) return value; if (value.length() < length) { String tmp = value; while (tmp.length() != length) { if (beginning) { tmp = c + tmp; } else { tmp = tmp + c; } } return tmp; } else if (shorten) { logger.warn(MessageFormat.format("Shortening length of value: {0}", value)); //$NON-NLS-1$ logger.warn(MessageFormat.format("Length is: {0} max: {1}", value.length(), length)); //$NON-NLS-1$ return value.substring(0, length); } return value; } /** * Calls {@link #replacePropertiesIn(Properties, String)}, with {@link System#getProperties()} as input * * @return a new string with all defined system properties replaced or if an error occurred the original value is * returned */ public static String replaceSystemPropertiesIn(String value) { return replacePropertiesIn(System.getProperties(), value); } /** * Traverses the given string searching for occurrences of ${...} sequences. Theses sequences are replaced with a * {@link Properties#getProperty(String)} value if such a value exists in the properties map. If the value of the * sequence is not in the properties, then the sequence is not replaced * * @param properties * the {@link Properties} in which to get the value * @param value * the value in which to replace any system properties * * @return a new string with all defined properties replaced or if an error occurred the original value is returned */ public static String replacePropertiesIn(Properties properties, String value) { return replacePropertiesIn(properties, '$', value); } /** * Traverses the given string searching for occurrences of <code>prefix</code>{...} sequences. Theses sequences are * replaced with a {@link Properties#getProperty(String)} value if such a value exists in the properties map. If the * value of the sequence is not in the properties, then the sequence is not replaced * * @param properties * the {@link Properties} in which to get the value * @param prefix * the prefix to use, for instance use <code>$</code> to replace occurrences of <code>$</code>{...} * @param value * the value in which to replace any system properties * * @return a new string with all defined properties replaced or if an error occurred the original value is returned */ public static String replacePropertiesIn(Properties properties, char prefix, String value) { String prefixS = String.valueOf(prefix); // get a copy of the value String tmpValue = value; // get first occurrence of $ character int pos = -1; int stop = 0; // loop on $ character positions while ((pos = tmpValue.indexOf(prefix, pos + 1)) != -1) { // if pos+1 is not { character then continue if (tmpValue.charAt(pos + 1) != '{') { continue; } // find end of sequence with } character stop = tmpValue.indexOf('}', pos + 1); // if no stop found, then break as another sequence should be able to start if (stop == -1) { logger.error(MessageFormat.format("Sequence starts at offset {0} but does not end!", pos)); //$NON-NLS-1$ tmpValue = value; break; } // get sequence enclosed by pos and stop String sequence = tmpValue.substring(pos + 2, stop); // make sure sequence doesn't contain $ { } characters if (sequence.contains(prefixS) || sequence.contains("{") || sequence.contains("}")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ String msg = "Enclosed sequence in offsets {0} - {1} contains one of the illegal chars: {2} { }: {3}"; msg = MessageFormat.format(msg, pos, stop, prefixS, sequence); logger.error(msg); tmpValue = value; break; } // sequence is good, so see if we have a property for it String property = properties.getProperty(sequence, StringHelper.EMPTY); // if no property exists, then log and continue if (property.isEmpty()) { // logger.warn("No system property found for sequence " + sequence); continue; } // property exists, so replace in value tmpValue = tmpValue.replace(prefixS + "{" + sequence + "}", property); //$NON-NLS-1$ //$NON-NLS-2$ } return tmpValue; } /** * Calls {@link #replaceProperties(Properties, Properties)} with null as the second argument. This allows for * replacing all properties with itself * * @param properties * the properties in which the values must have any ${...} replaced by values of the respective key */ public static void replaceProperties(Properties properties) { replaceProperties(properties, null); } /** * Checks every value in the {@link Properties} and then then replaces any ${...} variables with keys in this * {@link Properties} value using {@link StringHelper#replacePropertiesIn(Properties, String)} * * @param properties * the properties in which the values must have any ${...} replaced by values of the respective key * @param altProperties * if properties does not contain the ${...} key, then try these alternative properties */ public static void replaceProperties(Properties properties, Properties altProperties) { for (Object keyObj : properties.keySet()) { String key = (String) keyObj; String property = properties.getProperty(key); String newProperty = replacePropertiesIn(properties, property); // try first properties if (!property.equals(newProperty)) { // logger.info("Key " + key + " has replaced property " + property + " with new value " + newProperty); properties.put(key, newProperty); } else if (altProperties != null) { // try alternative properties newProperty = replacePropertiesIn(altProperties, property); if (!property.equals(newProperty)) { // logger.info("Key " + key + " has replaced property " + property + " from alternative properties with new value " + newProperty); properties.put(key, newProperty); } } } } /** * This is a helper method with which it is possible to print the location in the two given strings where they start * to differ. The length of string returned is currently 40 characters, or less if either of the given strings are * shorter. The format of the string is 3 lines. The first line has information about where in the strings the * difference occurs, and the second and third lines contain contexts * * @param s1 * the first string * @param s2 * the second string * * @return the string from which the strings differ with a length of 40 characters within the original strings */ public static String printUnequalContext(String s1, String s2) { byte[] bytes1 = s1.getBytes(); byte[] bytes2 = s2.getBytes(); int i = 0; for (; i < bytes1.length; i++) { if (i > bytes2.length) break; if (bytes1[i] != bytes2[i]) break; } int maxContext = 40; int start = Math.max(0, (i - maxContext)); int end = Math.min(i + maxContext, (Math.min(bytes1.length, bytes2.length))); StringBuilder sb = new StringBuilder(); sb.append("Strings are not equal! Start of inequality is at " + i); //$NON-NLS-1$ sb.append(". Showing " + maxContext); //$NON-NLS-1$ sb.append(" extra characters and start and end:\n"); //$NON-NLS-1$ sb.append("context s1: "); //$NON-NLS-1$ sb.append(s1.substring(start, end)); sb.append("\n"); //$NON-NLS-1$ sb.append("context s2: "); //$NON-NLS-1$ sb.append(s2.substring(start, end)); sb.append("\n"); //$NON-NLS-1$ return sb.toString(); } /** * Formats the given number of milliseconds to a time like #h/m/s/ms/us/ns * * @param millis * the number of milliseconds * * @return format the given number of milliseconds to a time like #h/m/s/ms/us/ns */ public static String formatMillisecondsDuration(final long millis) { return formatNanoDuration(millis * 1000000L); } /** * Formats the given number of nanoseconds to a time like #h/m/s/ms/us/ns * * @param nanos * the number of nanoseconds * * @return format the given number of nanoseconds to a time like #h/m/s/ms/us/ns */ public static String formatNanoDuration(final long nanos) { if (nanos >= 3600000000000L) { return String.format("%.0fh", (nanos / 3600000000000.0D)); //$NON-NLS-1$ } else if (nanos >= 60000000000L) { return String.format("%.0fm", (nanos / 60000000000.0D)); //$NON-NLS-1$ } else if (nanos >= 1000000000L) { return String.format("%.0fs", (nanos / 1000000000.0D)); //$NON-NLS-1$ } else if (nanos >= 1000000L) { return String.format("%.0fms", (nanos / 1000000.0D)); //$NON-NLS-1$ } else if (nanos >= 1000L) { return String.format("%.0fus", (nanos / 1000.0D)); //$NON-NLS-1$ } else { return nanos + "ns"; //$NON-NLS-1$ } } /** * @see ExceptionHelper#formatException(Throwable) */ public static String getExceptionMessage(Throwable t) { return ExceptionHelper.getExceptionMessage(t); } /** * @see ExceptionHelper#formatException(Throwable) */ public static String formatException(Throwable t) { return ExceptionHelper.formatException(t); } /** * @see ExceptionHelper#formatExceptionMessage(Throwable) */ public static String formatExceptionMessage(Throwable t) { return ExceptionHelper.formatExceptionMessage(t); } /** * Simply returns true if the value is null, or empty * * @param value * the value to check * * @return true if the value is null, or empty */ public static boolean isEmpty(String value) { return value == null || value.isEmpty(); } /** * Simply returns true if the value is neither null nor empty * * @param value * the value to check * * @return true if the value is neither null nor empty */ public static boolean isNotEmpty(String value) { return value != null && !value.isEmpty(); } /** * <p> * Parses the given string value to a boolean. This extends the default {@link Boolean#parseBoolean(String)} as it * throws an exception if the string value is not equal to "true" or "false" being case insensitive. * </p> * * <p> * This additional restriction is important where false should really be caught, not any random vaue for false * </p> * * @param value * the value to check * * @return true or false, depending on the string value * * @throws RuntimeException * if the value is empty, or not equal to the case insensitive value "true" or "false" */ public static boolean parseBoolean(String value) throws RuntimeException { if (isEmpty(value)) throw new RuntimeException("Value to parse to boolean is empty! Expected case insensitive true or false"); //$NON-NLS-1$ String tmp = value.toLowerCase(); if (tmp.equals(Boolean.TRUE.toString())) { return true; } else if (tmp.equals(Boolean.FALSE.toString())) { return false; } else { String msg = "Value {0} can not be parsed to boolean! Expected case insensitive true or false"; //$NON-NLS-1$ msg = MessageFormat.format(msg, value); throw new RuntimeException(msg); } } public static String commaSeparated(String... values) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < values.length; i++) { sb.append(values[i]); if (i < values.length - 1) sb.append(", "); //$NON-NLS-1$ } return sb.toString(); } public static String[] splitCommaSeparated(String values) { String[] split = values.split(","); //$NON-NLS-1$ for (int i = 0; i < split.length; i++) { split[i] = split[i].trim(); } return split; } /** * If the value parameter is empty, then a {@link #DASH} is returned, otherwise the value is returned * * @param value * * @return the non-empty value, or a {@link #DASH} */ public static String valueOrDash(String value) { if (isNotEmpty(value)) return value; return DASH; } /** * Return a pseudo unique id which is incremented on each call. The id is initialized from the current time * * @return a pseudo unique id */ public static synchronized String getUniqueId() { return Long.toString(getUniqueIdLong()); } /** * Return a pseudo unique id which is incremented on each call. The id is initialized from the current time * * @return a pseudo unique id */ public static synchronized long getUniqueIdLong() { if (uniqueId == Long.MAX_VALUE - 1) { uniqueId = 0; } uniqueId += 1; return uniqueId; } }