# ExpandableSQL.java

    package org.codefilarete.stalactite.sql.statement;

    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Map.Entry;

    import org.codefilarete.tool.Strings;
    import org.codefilarete.stalactite.sql.statement.SQLParameterParser.Parameter;
    import org.codefilarete.stalactite.sql.statement.SQLParameterParser.ParsedSQL;

    /**
     * Class that helps to adapt an SQL String with named parameters to a prepared statement, according to the values of those parameters :
     * It manages SQL expansion for parameters that have Collection value ('?' are added as many as necessary)
     * Kind of wrapper over {@link ParsedSQL}, delegates parsing to {@link SQLParameterParser}.
     *
     * @author Guillaume Mary
     */
    public class ExpandableSQL {

    	public static Map<String, Integer> sizes(Map<String, Object> values) {
    		HashMap<String, Integer> toReturn = new HashMap<>();
    		for (Entry<String, Object> entry : values.entrySet()) {
    			toReturn.put(entry.getKey(), entry.getValue() instanceof Collection ? ((Collection) entry.getValue()).size() : 1);
    		}
    		return toReturn;
    	}

    	private final Map<String, ExpandableParameter> expandableParameters;
    	private final ParsedSQL parsedSQL;

    	private String preparedSQL;

    	public ExpandableSQL(ParsedSQL parsedSQL, Map<String, Integer> parameterValuesSize) {
    		this.parsedSQL = parsedSQL;
    		this.expandableParameters = new HashMap<>(this.parsedSQL.getSqlSnippets().size() / 2);
    		convertParsedParametersToExpandableParameters(parameterValuesSize);
    	}

    	private void convertParsedParametersToExpandableParameters(Map<String, Integer> parameterValuesSize) {
    		StringBuilder preparedSQLBuilder = new StringBuilder();
    		// index for prepared statement mark (?), used to compute mark indexes of parameters
    		int markIndex = 1;
    		for (Object sqlSnippet : parsedSQL.getSqlSnippets()) {
    			if (sqlSnippet instanceof Parameter) {
    				String parameterName = ((Parameter) sqlSnippet).getName();
    				Integer valueSize = parameterValuesSize.get(parameterName);
    				if (valueSize == null) {
    					throw new IllegalArgumentException("Size is not given for parameter " + parameterName + " hence expansion is not possible");
    				} else {
    					buildParameter(parameterName, valueSize, markIndex, preparedSQLBuilder);
    					markIndex += valueSize;
    				}
    			} else {
    				// sql snippet
    				preparedSQLBuilder.append(sqlSnippet);
    			}
    		}
    		this.preparedSQL = preparedSQLBuilder.toString();
    	}

    	private void buildParameter(String parameterName, int valueSize, int firstIndex, StringBuilder preparedSQLBuilder) {
    		ExpandableParameter expandableParameter = this.expandableParameters.computeIfAbsent(parameterName, name -> new ExpandableParameter(name, valueSize));
    		expandableParameter.buildMarkIndexes(firstIndex);
    		expandableParameter.catParameterMarks(preparedSQLBuilder);
    	}

    	public Map<String, ExpandableParameter> getExpandableParameters() {
    		return expandableParameters;
    	}

    	public String getPreparedSQL() {
    		return preparedSQL;
    	}

    	/**
    	 * Class that represents a named parameter in a SQL statement.
    	 * It allows extension of single parameter mark (coded as a question mark '?') to multiple ones in case of
    	 * Collection value. This is done on the {@link #catParameterMarks(StringBuilder)} method.
    	 */
    	public static class ExpandableParameter {

    		public static final String SQL_PARAMETER_MARK = "?";

    		public static final String SQL_PARAMETER_SEPARATOR = ", ";

    		public static final String SQL_PARAMETER_MARK_1 = SQL_PARAMETER_MARK + SQL_PARAMETER_SEPARATOR;

    		public static final String SQL_PARAMETER_MARK_10 =
    				SQL_PARAMETER_MARK_1 + SQL_PARAMETER_MARK_1 +
    						SQL_PARAMETER_MARK_1 + SQL_PARAMETER_MARK_1 +
    						SQL_PARAMETER_MARK_1 + SQL_PARAMETER_MARK_1 +
    						SQL_PARAMETER_MARK_1 + SQL_PARAMETER_MARK_1 +
    						SQL_PARAMETER_MARK_1 + SQL_PARAMETER_MARK_1;

    		public static final String SQL_PARAMETER_MARK_100 =
    				SQL_PARAMETER_MARK_10 + SQL_PARAMETER_MARK_10 +
    						SQL_PARAMETER_MARK_10 + SQL_PARAMETER_MARK_10 +
    						SQL_PARAMETER_MARK_10 + SQL_PARAMETER_MARK_10 +
    						SQL_PARAMETER_MARK_10 + SQL_PARAMETER_MARK_10 +
    						SQL_PARAMETER_MARK_10 + SQL_PARAMETER_MARK_10;

    		/** The parameter name */
    		private final String parameterName;
    		/** PreparedStatement parameter mark count for this parameter, 1 for single value, N for Collection value */
    		private final int markCount;
    		/** Index of the parameter in the PreparedStatement, the first one for Collection value */
    		private int[] indexes;

    		private ExpandableParameter(String parameterName, int markCount) {
    			this.parameterName = parameterName;
    			this.markCount = markCount;
    		}

    		public String getParameterName() {
    			return parameterName;
    		}

    		/**
    		 * Gives all indexes of this instance.
    		 *
    		 * @return a one-sized array for single value parameter, a n-sized array for n-sized collection value parameter
    		 */
    		public int[] getMarkIndexes() {
    			return indexes;
    		}

    		private void buildMarkIndexes(int startIndex) {
    			int offset;
    			if (indexes == null) {
    				offset = 0;
    				indexes = new int[markCount];
    			} else {
    				// parameter already has indexes: we keep them (what a nice array copy !)
    				offset = indexes.length;
    				int[] newIndexes = new int[offset + markCount];
    				System.arraycopy(indexes, 0, newIndexes, 0, offset);
    				indexes = newIndexes;
    			}
    			// build mark indexes
    			for (int i=0; i < markCount; i++) {
    				indexes[i+offset] = startIndex++;
    			}
    		}

    		private StringBuilder catParameterMarks(StringBuilder stringBuilder) {
    			if (markCount > 1) {
    				return expandParameters(stringBuilder);
    			} else {
    				return stringBuilder.append(SQL_PARAMETER_MARK);
    			}
    		}

    		/**
    		 * Convert the single valued parameter to a multiple one: add as many '?' as necessary
    		 * @return the changed sql
    		 * @param stringBuilder
    		 */
    		protected StringBuilder expandParameters(StringBuilder stringBuilder) {
    			StringBuilder sqlParameters = Strings.repeat(stringBuilder, markCount, SQL_PARAMETER_MARK_1, SQL_PARAMETER_MARK_100, SQL_PARAMETER_MARK_10);
    			sqlParameters.setLength(sqlParameters.length() - 2);
    			return sqlParameters;
    		}
    	}
    }