/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.internal.util;

import java.io.Serializable;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;

import org.hibernate.boot.model.source.internal.hbm.CommaSeparatedStringHelper;
import org.hibernate.dialect.Dialect;
import org.hibernate.loader.internal.AliasConstantsHelper;

public final class StringHelper {

	private static final int ALIAS_TRUNCATE_LENGTH = 10;
	public static final String WHITESPACE = " \n\r\f\t";
	public static final String[] EMPTY_STRINGS = new String[0];

	private StringHelper() { /* static methods only - hide constructor */
	}

	public static int lastIndexOfLetter(String string) {
		for ( int i = 0; i < string.length(); i++ ) {
			char character = string.charAt( i );
			// Include "_".  See HHH-8073
			if ( !Character.isLetter( character ) && !( '_' == character ) ) {
				return i - 1;
			}
		}
		return string.length() - 1;
	}

	public static String joinWithQualifierAndSuffix(
			String[] values,
			String qualifier,
			String suffix,
			String deliminator) {
		int length = values.length;
		if ( length == 0 ) {
			return "";
		}
		StringBuilder buf = new StringBuilder( length * ( values[0].length() + suffix.length() ) )
				.append( qualify( qualifier, values[0] ) ).append( suffix );
		for ( int i = 1; i < length; i++ ) {
			buf.append( deliminator ).append( qualify( qualifier, values[i] ) ).append( suffix );
		}
		return buf.toString();
	}

	public static String join(String separator, Iterator<?> objects) {
		StringBuilder buf = new StringBuilder();
		if ( objects.hasNext() ) {
			buf.append( objects.next() );
		}
		while ( objects.hasNext() ) {
			buf.append( separator ).append( objects.next() );
		}
		return buf.toString();
	}

	public static String[] add(String[] x, String sep, String[] y) {
		final String[] result = new String[x.length];
		for ( int i = 0; i < x.length; i++ ) {
			result[i] = x[i] + sep + y[i];
		}
		return result;
	}

	public static String repeat(String string, int times) {
		StringBuilder buf = new StringBuilder( string.length() * times );
		for ( int i = 0; i < times; i++ ) {
			buf.append( string );
		}
		return buf.toString();
	}

	public static String repeat(String string, int times, String deliminator) {
		StringBuilder buf = new StringBuilder( ( string.length() * times ) + ( deliminator.length() * ( times - 1 ) ) )
				.append( string );
		for ( int i = 1; i < times; i++ ) {
			buf.append( deliminator ).append( string );
		}
		return buf.toString();
	}

	public static String repeat(char character, int times) {
		char[] buffer = new char[times];
		Arrays.fill( buffer, character );
		return new String( buffer );
	}

	public static String replace(String template, String placeholder, String replacement) {
		return replace( template, placeholder, replacement, false );
	}

	public static String[] replace(String[] templates, String placeholder, String replacement) {
		String[] result = new String[templates.length];
		for ( int i = 0; i < templates.length; i++ ) {
			result[i] = replace( templates[i], placeholder, replacement );
		}
		return result;
	}

	public static String replace(String template, String placeholder, String replacement, boolean wholeWords) {
		return replace( template, placeholder, replacement, wholeWords, false );
	}

	public static String replace(
			String template,
			String placeholder,
			String replacement,
			boolean wholeWords,
			boolean encloseInParensIfNecessary) {
		if ( template == null ) {
			return null;
		}
		int loc = indexOfPlaceHolder( template, placeholder, wholeWords );
		if ( loc < 0 ) {
			return template;
		}
		else {
			String beforePlaceholder = template.substring( 0, loc );
			String afterPlaceholder = template.substring( loc + placeholder.length() );
			return replace(
					beforePlaceholder,
					afterPlaceholder,
					placeholder,
					replacement,
					wholeWords,
					encloseInParensIfNecessary
			);
		}
	}

	public static String replace(
			String beforePlaceholder,
			String afterPlaceholder,
			String placeholder,
			String replacement,
			boolean wholeWords,
			boolean encloseInParensIfNecessary) {
		final boolean actuallyReplace =
				!wholeWords
						|| afterPlaceholder.length() == 0
						|| !Character.isJavaIdentifierPart( afterPlaceholder.charAt( 0 ) );
		// We only need to check the left param to determine if the placeholder is already
		// enclosed in parentheses (HHH-10383)
		// Examples:
		// 1) "... IN (?1", we assume that "?1" does not need to be enclosed because there
		// is already a right-parenthesis; we assume there will be a matching right-parenthesis.
		// 2) "... IN ?1", we assume that "?1" needs to be enclosed in parentheses, because there
		// is no left-parenthesis.

		// We need to check the placeholder is not used in `Order By FIELD(...)` (HHH-10502)
		// Examples:
		// " ... Order By FIELD(id,?1)",  after expand parameters, the sql is "... Order By FIELD(id,?,?,?)"
		boolean encloseInParens =
				actuallyReplace
						&& encloseInParensIfNecessary
						&& !( getLastNonWhitespaceCharacter( beforePlaceholder ) == '(' ) &&
						!( getLastNonWhitespaceCharacter( beforePlaceholder ) == ',' && getFirstNonWhitespaceCharacter(
								afterPlaceholder ) == ')' );
		StringBuilder buf = new StringBuilder( beforePlaceholder );
		if ( encloseInParens ) {
			buf.append( '(' );
		}
		buf.append( actuallyReplace ? replacement : placeholder );
		if ( encloseInParens ) {
			buf.append( ')' );
		}
		buf.append(
				replace(
						afterPlaceholder,
						placeholder,
						replacement,
						wholeWords,
						encloseInParensIfNecessary
				)
		);
		return buf.toString();
	}

	private static int indexOfPlaceHolder(String template, String placeholder, boolean wholeWords) {
		if ( wholeWords ) {
			int placeholderIndex = -1;
			boolean isPartialPlaceholderMatch;
			do {
				placeholderIndex = template.indexOf( placeholder, placeholderIndex + 1 );
				isPartialPlaceholderMatch = placeholderIndex != -1 &&
						template.length() > placeholderIndex + placeholder.length() &&
						Character.isJavaIdentifierPart( template.charAt( placeholderIndex + placeholder.length() ) );
			} while ( placeholderIndex != -1 && isPartialPlaceholderMatch );

			return placeholderIndex;
		}
		else {
			return template.indexOf( placeholder );
		}
	}

	/**
	 * Used to find the ordinal parameters (e.g. '?1') in a string.
	 */
	public static int indexOfIdentifierWord(String str, String word) {
		if ( str == null || str.length() == 0 || word == null || word.length() == 0 ) {
			return -1;
		}

		int position = str.indexOf( word );
		while ( position >= 0 && position < str.length() ) {
			if (
					( position == 0 || !Character.isJavaIdentifierPart( str.charAt( position - 1 ) ) ) &&
					( position + word.length() == str.length() || !Character.isJavaIdentifierPart( str.charAt( position + word.length() ) ) )
			) {
				return position;
			}
			position = str.indexOf( word, position + 1 );
		}

		return -1;
	}

	public static char getLastNonWhitespaceCharacter(String str) {
		if ( str != null && str.length() > 0 ) {
			for ( int i = str.length() - 1; i >= 0; i-- ) {
				char ch = str.charAt( i );
				if ( !Character.isWhitespace( ch ) ) {
					return ch;
				}
			}
		}
		return '\0';
	}

	public static char getFirstNonWhitespaceCharacter(String str) {
		if ( str != null && str.length() > 0 ) {
			for ( int i = 0; i < str.length(); i++ ) {
				char ch = str.charAt( i );
				if ( !Character.isWhitespace( ch ) ) {
					return ch;
				}
			}
		}
		return '\0';
	}

	public static String replaceOnce(String template, String placeholder, String replacement) {
		if ( template == null ) {
			return null;  // returning null!
		}
		int loc = template.indexOf( placeholder );
		if ( loc < 0 ) {
			return template;
		}
		else {
			return template.substring( 0, loc ) + replacement + template.substring( loc + placeholder.length() );
		}
	}


	public static String[] split(String separators, String list) {
		return split( separators, list, false );
	}

	public static String[] split(String separators, String list, boolean include) {
		StringTokenizer tokens = new StringTokenizer( list, separators, include );
		String[] result = new String[tokens.countTokens()];
		int i = 0;
		while ( tokens.hasMoreTokens() ) {
			result[i++] = tokens.nextToken();
		}
		return result;
	}

	public static String[] splitTrimmingTokens(String separators, String list, boolean include) {
		StringTokenizer tokens = new StringTokenizer( list, separators, include );
		String[] result = new String[tokens.countTokens()];
		int i = 0;
		while ( tokens.hasMoreTokens() ) {
			result[i++] = tokens.nextToken().trim();
		}
		return result;
	}

	public static String unqualify(String qualifiedName) {
		int loc = qualifiedName.lastIndexOf( '.' );
		return ( loc < 0 ) ? qualifiedName : qualifiedName.substring( loc + 1 );
	}

	public static String qualifier(String qualifiedName) {
		int loc = qualifiedName.lastIndexOf( '.' );
		return ( loc < 0 ) ? "" : qualifiedName.substring( 0, loc );
	}

	/**
	 * Collapses a name.  Mainly intended for use with classnames, where an example might serve best to explain.
	 * Imagine you have a class named <samp>'org.hibernate.internal.util.StringHelper'</samp>; calling collapse on that
	 * classname will result in <samp>'o.h.u.StringHelper'</samp>.
	 *
	 * @param name The name to collapse.
	 *
	 * @return The collapsed name.
	 */
	public static String collapse(String name) {
		if ( name == null ) {
			return null;
		}
		int breakPoint = name.lastIndexOf( '.' );
		if ( breakPoint < 0 ) {
			return name;
		}
		return collapseQualifier(
				name.substring( 0, breakPoint ),
				true
		) + name.substring( breakPoint ); // includes last '.'
	}

	/**
	 * Given a qualifier, collapse it.
	 *
	 * @param qualifier The qualifier to collapse.
	 * @param includeDots Should we include the dots in the collapsed form?
	 *
	 * @return The collapsed form.
	 */
	public static String collapseQualifier(String qualifier, boolean includeDots) {
		StringTokenizer tokenizer = new StringTokenizer( qualifier, "." );
		StringBuilder sb = new StringBuilder();
		sb.append( Character.toString( tokenizer.nextToken().charAt( 0 ) ) );
		while ( tokenizer.hasMoreTokens() ) {
			if ( includeDots ) {
				sb.append( '.' );
			}
			sb.append( tokenizer.nextToken().charAt( 0 ) );
		}
		return sb.toString();
	}

	/**
	 * Partially unqualifies a qualified name.  For example, with a base of 'org.hibernate' the name
	 * 'org.hibernate.internal.util.StringHelper' would become 'util.StringHelper'.
	 *
	 * @param name The (potentially) qualified name.
	 * @param qualifierBase The qualifier base.
	 *
	 * @return The name itself, or the partially unqualified form if it begins with the qualifier base.
	 */
	public static String partiallyUnqualify(String name, String qualifierBase) {
		if ( name == null || !name.startsWith( qualifierBase ) ) {
			return name;
		}
		return name.substring( qualifierBase.length() + 1 ); // +1 to start after the following '.'
	}

	/**
	 * Cross between {@link #collapse} and {@link #partiallyUnqualify}.  Functions much like {@link #collapse}
	 * except that only the qualifierBase is collapsed.  For example, with a base of 'org.hibernate' the name
	 * 'org.hibernate.internal.util.StringHelper' would become 'o.h.util.StringHelper'.
	 *
	 * @param name The (potentially) qualified name.
	 * @param qualifierBase The qualifier base.
	 *
	 * @return The name itself if it does not begin with the qualifierBase, or the properly collapsed form otherwise.
	 */
	public static String collapseQualifierBase(String name, String qualifierBase) {
		if ( name == null || !name.startsWith( qualifierBase ) ) {
			return collapse( name );
		}
		return collapseQualifier( qualifierBase, true ) + name.substring( qualifierBase.length() );
	}

	public static String[] suffix(String[] columns, String suffix) {
		if ( suffix == null ) {
			return columns;
		}
		String[] qualified = new String[columns.length];
		for ( int i = 0; i < columns.length; i++ ) {
			qualified[i] = suffix( columns[i], suffix );
		}
		return qualified;
	}

	private static String suffix(String name, String suffix) {
		return ( suffix == null ) ? name : name + suffix;
	}

	public static String root(String qualifiedName) {
		int loc = qualifiedName.indexOf( '.' );
		return ( loc < 0 ) ? qualifiedName : qualifiedName.substring( 0, loc );
	}

	public static String unroot(String qualifiedName) {
		int loc = qualifiedName.indexOf( '.' );
		return ( loc < 0 ) ? qualifiedName : qualifiedName.substring( loc + 1, qualifiedName.length() );
	}

	public static String toString(Object[] array) {
		int len = array.length;
		if ( len == 0 ) {
			return "";
		}
		StringBuilder buf = new StringBuilder( len * 12 );
		for ( int i = 0; i < len - 1; i++ ) {
			buf.append( array[i] ).append( ", " );
		}
		return buf.append( array[len - 1] ).toString();
	}

	public static String[] multiply(String string, Iterator<String> placeholders, Iterator<String[]> replacements) {
		String[] result = new String[] {string};
		while ( placeholders.hasNext() ) {
			result = multiply( result, placeholders.next(), replacements.next() );
		}
		return result;
	}

	private static String[] multiply(String[] strings, String placeholder, String[] replacements) {
		String[] results = new String[replacements.length * strings.length];
		int n = 0;
		for ( String replacement : replacements ) {
			for ( String string : strings ) {
				results[n++] = replaceOnce( string, placeholder, replacement );
			}
		}
		return results;
	}

	public static int countUnquoted(String string, char character) {
		if ( '\'' == character ) {
			throw new IllegalArgumentException( "Unquoted count of quotes is invalid" );
		}
		if ( string == null ) {
			return 0;
		}
		// Impl note: takes advantage of the fact that an escaped single quote
		// embedded within a quote-block can really be handled as two separate
		// quote-blocks for the purposes of this method...
		int count = 0;
		int stringLength = string.length();
		boolean inQuote = false;
		for ( int indx = 0; indx < stringLength; indx++ ) {
			char c = string.charAt( indx );
			if ( inQuote ) {
				if ( '\'' == c ) {
					inQuote = false;
				}
			}
			else if ( '\'' == c ) {
				inQuote = true;
			}
			else if ( c == character ) {
				count++;
			}
		}
		return count;
	}

	public static boolean isNotEmpty(String string) {
		return string != null && string.length() > 0;
	}

	public static boolean isEmpty(String string) {
		return string == null || string.isEmpty();
	}

	public static boolean isBlank(String string) {
		//TODO use Java 11's more efficient String#isBlank - currently we still require Java 8 compatibility
		if ( string == null || string.isEmpty() ) {
			return true;
		}
		else {
			//Else: we need to check all characters, preferably without using String#trim() so to
			//not allocate temporary strings
			for ( int i = 0; i < string.length(); i++ ) {
				if ( ! Character.isWhitespace( string.charAt( i ) ) ) {
					return false;
				}
			}
			return true;
		}
	}

	/**
	 * @deprecated use {@link #isBlank(String)}
	 * @return
	 */
	@Deprecated
	public static boolean isEmptyOrWhitespace(String string) {
		return isBlank(string);
	}

	public static String qualify(String prefix, String name) {
		if ( name == null || prefix == null ) {
			throw new NullPointerException( "prefix or name were null attempting to build qualified name" );
		}
		return prefix + '.' + name;
	}

	public static String qualifyConditionally(String prefix, String name) {
		if ( name == null ) {
			throw new NullPointerException( "name was null attempting to build qualified name" );
		}
		return isEmpty( prefix ) ? name : prefix + '.' + name;
	}

	public static String[] qualify(String prefix, String[] names) {
		if ( prefix == null ) {
			return names;
		}
		int len = names.length;
		String[] qualified = new String[len];
		for ( int i = 0; i < len; i++ ) {
			qualified[i] = qualify( prefix, names[i] );
		}
		return qualified;
	}

	public static int firstIndexOfChar(String sqlString, BitSet keys, int startindex) {
		for ( int i = startindex, size = sqlString.length(); i < size; i++ ) {
			if ( keys.get( sqlString.charAt( i ) ) ) {
				return i;
			}
		}
		return -1;
	}

	public static int firstIndexOfChar(String sqlString, String string, int startindex) {
		BitSet keys = new BitSet();
		for ( int i = 0, size = string.length(); i < size; i++ ) {
			keys.set( string.charAt( i ) );
		}
		return firstIndexOfChar( sqlString, keys, startindex );
	}

	public static String truncate(String string, int length) {
		if ( string.length() <= length ) {
			return string;
		}
		else {
			return string.substring( 0, length );
		}
	}

	public static String generateAlias(String description) {
		return generateAliasRoot( description ) + '_';
	}

	/**
	 * Generate a nice alias for the given class name or collection role name and unique integer. Subclasses of
	 * Loader do <em>not</em> have to use aliases of this form.
	 *
	 * @param description The base name (usually an entity-name or collection-role)
	 * @param unique A uniquing value
	 *
	 * @return an alias of the form <samp>foo1_</samp>
	 */
	public static String generateAlias(String description, int unique) {
		return generateAliasRoot( description )
				+ AliasConstantsHelper.get( unique );
	}

	/**
	 * Generates a root alias by truncating the "root name" defined by
	 * the incoming description and removing/modifying any non-valid
	 * alias characters.
	 *
	 * @param description The root name from which to generate a root alias.
	 *
	 * @return The generated root alias.
	 */
	private static String generateAliasRoot(String description) {
		String result = truncate( unqualifyEntityName( description ), ALIAS_TRUNCATE_LENGTH )
				.toLowerCase( Locale.ROOT )
				.replace( '/', '_' ) // entityNames may now include slashes for the representations
				.replace( '$', '_' ); //classname may be an inner class
		result = cleanAlias( result );
		if ( Character.isDigit( result.charAt( result.length() - 1 ) ) ) {
			return result + "x"; //ick!
		}
		else {
			return result;
		}
	}

	/**
	 * Clean the generated alias by removing any non-alpha characters from the
	 * beginning.
	 *
	 * @param alias The generated alias to be cleaned.
	 *
	 * @return The cleaned alias, stripped of any leading non-alpha characters.
	 */
	private static String cleanAlias(String alias) {
		char[] chars = alias.toCharArray();
		// shortcut check...
		if ( !Character.isLetter( chars[0] ) ) {
			for ( int i = 1; i < chars.length; i++ ) {
				// as soon as we encounter our first letter, return the substring
				// from that position
				if ( Character.isLetter( chars[i] ) ) {
					return alias.substring( i );
				}
			}
		}
		return alias;
	}

	public static String unqualifyEntityName(String entityName) {
		String result = unqualify( entityName );
		int slashPos = result.indexOf( '/' );
		if ( slashPos > 0 ) {
			result = result.substring( 0, slashPos - 1 );
		}
		return result;
	}

	public static String moveAndToBeginning(String filter) {
		if ( !isBlank( filter ) ) {
			filter += " and ";
			if ( filter.startsWith( " and " ) ) {
				filter = filter.substring( 4 );
			}
		}
		return filter;
	}

	/**
	 * Determine if the given string is quoted (wrapped by '`' characters at beginning and end).
	 *
	 * @param name The name to check.
	 *
	 * @return True if the given string starts and ends with '`'; false otherwise.
	 */
	public static boolean isQuoted(final String name) {
		if ( name == null || name.isEmpty() ) {
			return false;
		}

		final char first = name.charAt( 0 );
		final char last = name.charAt( name.length() - 1 );

		return ( ( first == last ) && ( first == '`' || first == '"' ) );
	}

	/**
	 * Return the unquoted version of name (stripping the start and end '`' characters if present).
	 *
	 * @param name The name to be unquoted.
	 *
	 * @return The unquoted version.
	 */
	public static String unquote(String name) {
		return isQuoted( name ) ? name.substring( 1, name.length() - 1 ) : name;
	}

	/**
	 * Determine if the given name is quoted.  It is considered quoted if either:
	 * <ol>
	 * <li>starts AND ends with backticks (`)</li>
	 * <li>starts with dialect-specified {@link Dialect#openQuote() open-quote}
	 * AND ends with dialect-specified {@link Dialect#closeQuote() close-quote}</li>
	 * </ol>
	 *
	 * @param name The name to check
	 * @param dialect The dialect (to determine the "real" quoting chars).
	 *
	 * @return True if quoted, false otherwise
	 */
	public static boolean isQuoted(final String name, final Dialect dialect) {
		if ( name == null || name.isEmpty() ) {
			return false;
		}
		final char first = name.charAt( 0 );
		final char last = name.charAt( name.length() - 1 );

		return ( ( first == last ) && ( first == '`' || first == '"' ) )
				|| ( first == dialect.openQuote() && last == dialect.closeQuote() );
	}

	/**
	 * Return the unquoted version of name stripping the start and end quote characters.
	 *
	 * @param name The name to be unquoted.
	 * @param dialect The dialect (to determine the "real" quoting chars).
	 *
	 * @return The unquoted version.
	 */
	public static String unquote(String name, Dialect dialect) {
		return isQuoted( name, dialect ) ? name.substring( 1, name.length() - 1 ) : name;
	}

	/**
	 * Return the unquoted version of name stripping the start and end quote characters.
	 *
	 * @param names The names to be unquoted.
	 * @param dialect The dialect (to determine the "real" quoting chars).
	 *
	 * @return The unquoted versions.
	 */
	public static String[] unquote(final String[] names, final Dialect dialect) {
		if ( names == null ) {
			return null;
		}
		int failedIndex = -1;
		final int length = names.length;
		for ( int i = 0; i < length; i++ ) {
			if ( isQuoted( names[i], dialect ) ) {
				failedIndex = i;
				break;
			}
		}
		if ( failedIndex == -1 ) {
			//In this case all strings are already unquoted, so return the same array as the input:
			//this is a good optimisation to skip an array copy as typically either all names are consistently quoted, or none are;
			//yet for safety we need to deal with mixed scenarios as well.
			return names;
		}
		else {
			String[] unquoted = new String[length];
			System.arraycopy( names, 0, unquoted, 0, failedIndex );
			for ( int i = failedIndex; i < length; i++ ) {
				unquoted[i] = unquote( names[i], dialect );
			}
			return unquoted;
		}
	}


	public static final String BATCH_ID_PLACEHOLDER = "$$BATCH_ID_PLACEHOLDER$$";

	public static StringBuilder buildBatchFetchRestrictionFragment(
			String alias,
			String[] columnNames,
			Dialect dialect) {
		// the general idea here is to just insert a placeholder that we can easily find later...
		if ( columnNames.length == 1 ) {
			// non-composite key
			return new StringBuilder( StringHelper.qualify( alias, columnNames[0] ) )
					.append( " in (" ).append( BATCH_ID_PLACEHOLDER ).append( ')' );
		}
		else {
			// composite key - the form to use here depends on what the dialect supports.
			if ( dialect.supportsRowValueConstructorSyntaxInInList() ) {
				// use : (col1, col2) in ( (?,?), (?,?), ... )
				StringBuilder builder = new StringBuilder();
				builder.append( '(' );
				boolean firstPass = true;
				String deliminator = "";
				for ( String columnName : columnNames ) {
					builder.append( deliminator ).append( StringHelper.qualify( alias, columnName ) );
					if ( firstPass ) {
						firstPass = false;
						deliminator = ",";
					}
				}
				builder.append( ") in (" );
				builder.append( BATCH_ID_PLACEHOLDER );
				builder.append( ')' );
				return builder;
			}
			else {
				// use : ( (col1 = ? and col2 = ?) or (col1 = ? and col2 = ?) or ... )
				//		unfortunately most of this building needs to be held off until we know
				//		the exact number of ids :(
				final StringBuilder stringBuilder = new StringBuilder();
				stringBuilder.append( '(' )
						.append( BATCH_ID_PLACEHOLDER )
						.append( ')' );
				return stringBuilder;
			}
		}
	}

	public static String expandBatchIdPlaceholder(
			String sql,
			Serializable[] ids,
			String alias,
			String[] keyColumnNames,
			Dialect dialect) {
		if ( keyColumnNames.length == 1 ) {
			// non-composite
			return StringHelper.replace( sql, BATCH_ID_PLACEHOLDER, repeat( "?", ids.length, "," ) );
		}
		else {
			// composite
			if ( dialect.supportsRowValueConstructorSyntaxInInList() ) {
				final String tuple = '(' + StringHelper.repeat( "?", keyColumnNames.length, "," ) + ')';
				return StringHelper.replace( sql, BATCH_ID_PLACEHOLDER, repeat( tuple, ids.length, "," ) );
			}
			else {
				final String keyCheck = '(' + joinWithQualifierAndSuffix(
						keyColumnNames,
						alias,
						" = ?",
						" and "
				) + ')';
				return replace( sql, BATCH_ID_PLACEHOLDER, repeat( keyCheck, ids.length, " or " ) );
			}
		}
	}

	public static String nullIfEmpty(String value) {
		return isEmpty( value ) ? null : value;
	}

	public static List<String> parseCommaSeparatedString(String incomingString) {
		return CommaSeparatedStringHelper.parseCommaSeparatedString( incomingString );
	}

	public static <T> String join(Collection<T> values, Renderer<T> renderer) {
		final StringBuilder buffer = new StringBuilder();
		for ( T value : values ) {
			buffer.append( String.join(", ", renderer.render( value ) ) );
		}
		return buffer.toString();
	}

	public interface Renderer<T> {
		String render(T value);
	}

	/**
	 * @param firstExpression the first expression
	 * @param secondExpression the second expression
	 * @return if {@code firstExpression} and {@code secondExpression} are both non-empty,
	 * then "( " + {@code firstExpression} + " ) and ( " + {@code secondExpression} + " )" is returned;
	 * if {@code firstExpression} is non-empty and {@code secondExpression} is empty,
	 * then {@code firstExpression} is returned;
	 * if {@code firstExpression} is empty and {@code secondExpression} is non-empty,
	 * then {@code secondExpression} is returned;
	 * if both {@code firstExpression} and {@code secondExpression} are empty, then null is returned.
	 */
	public static String getNonEmptyOrConjunctionIfBothNonEmpty( String firstExpression, String secondExpression ) {
		final boolean isFirstExpressionNonEmpty = StringHelper.isNotEmpty( firstExpression );
		final boolean isSecondExpressionNonEmpty = StringHelper.isNotEmpty( secondExpression );
		if ( isFirstExpressionNonEmpty && isSecondExpressionNonEmpty ) {
			final StringBuilder buffer = new StringBuilder();
			buffer.append( "( " )
					.append( firstExpression )
					.append( " ) and ( ")
					.append( secondExpression )
					.append( " )" );
			return buffer.toString();
		}
		else if ( isFirstExpressionNonEmpty ) {
			return firstExpression;
		}
		else if ( isSecondExpressionNonEmpty ) {
			return secondExpression;
		}
		else {
			return null;
		}
	}

	/**
	 * Return the interned form of a String, or null if the parameter is null.
	 * <p>
	 * Use with caution: excessive interning is known to cause issues.
	 * Best to use only with strings which are known to be long lived constants,
	 * and for which the chances of being actual duplicates is proven.
	 * (Even better: avoid needing interning by design changes such as reusing
	 * the known reference)
	 * @param string The string to intern.
	 * @return The interned string.
	 */
	public static String safeInterning(final String string) {
		if ( string == null ) {
			return null;
		}
		else {
			return string.intern();
		}
	}

}
