001package com.identityworksllc.iiq.common.query; 002 003import java.sql.CallableStatement; 004import java.sql.Connection; 005import java.sql.PreparedStatement; 006import java.sql.SQLException; 007import java.sql.Statement; 008import java.util.Collections; 009import java.util.HashMap; 010import java.util.LinkedList; 011import java.util.List; 012import java.util.Map; 013import java.util.Set; 014 015/** 016 * The abstract superclass for the named parameter statement types. 017 * 018 * @param <StatementType> The type of statement, {@link PreparedStatement} or {@link CallableStatement} 019 */ 020public abstract class AbstractNamedParameterStatement<StatementType extends Statement> implements AutoCloseable { 021 022 /** 023 * Parses a query with named parameters. The parameter-index mappings are put into the map, and the parsed query is returned. 024 * 025 * @param query query to parse 026 * @param indexMap map to hold parameter-index mappings 027 * @return the parsed query 028 */ 029 public static String parse(String query, Map<String, int[]> indexMap) { 030 Map<String, List<Integer>> paramMap = new HashMap<>(); 031 int length = query.length(); 032 StringBuilder parsedQuery = new StringBuilder(length); 033 int index = 1; 034 for (int i = 0; i < length; i++) { 035 char c = query.charAt(i); 036 if (c == '\'' || c == '"') { 037 // Consume quoted substrings... 038 char original = c; 039 do { 040 i++; 041 parsedQuery.append(c); 042 } while (i < length && (c = query.charAt(i)) != original); 043 } else if (c == ':' && i + 1 < length && Character.isJavaIdentifierStart(query.charAt(i + 1))) { 044 // Found a placeholder! 045 String name = parseParameterName(query, i); 046 c = '?'; // replace the parameter with a question mark 047 i += name.length(); // skip past the end if the parameter 048 List<Integer> indexList = paramMap.computeIfAbsent(name, k -> new LinkedList<>()); 049 indexList.add(index); 050 index++; 051 } 052 parsedQuery.append(c); 053 } 054 toIntArrayMap(paramMap, indexMap); 055 return parsedQuery.toString(); 056 } 057 058 /** 059 * Parses a name from the given query string starting at the given position. 060 * 061 * @param query The query string from which to parse the parameter name 062 * @param pos The position at which it was detected a parameter starts 063 * @return The name of the parameter parsed 064 */ 065 private static String parseParameterName(String query, int pos) { 066 int j = pos + 2; 067 while (j < query.length() && Character.isJavaIdentifierPart(query.charAt(j))) { 068 j++; 069 } 070 return query.substring(pos + 1, j); 071 } 072 073 /** 074 * Moves all values from a map having a list of ints, to one having an array of ints 075 * 076 * @param inMap The input map, having a list of ints for values. 077 * @param outMap The output map, on which to put the same values as an array of ints. 078 */ 079 private static void toIntArrayMap(Map<String, List<Integer>> inMap, Map<String, int[]> outMap) { 080 // replace the lists of Integer objects with arrays of ints 081 for (Map.Entry<String, List<Integer>> entry : inMap.entrySet()) { 082 List<Integer> list = entry.getValue(); 083 int[] indexes = new int[list.size()]; 084 int i = 0; 085 for (Integer integer : list) { 086 indexes[i++] = integer; 087 } 088 outMap.put(entry.getKey(), indexes); 089 } 090 } 091 092 /** 093 * If true, invoking one of the setXXX methods on a non-existent parameter 094 * will be silently ignored. The default is to throw an exception. 095 */ 096 protected boolean allowMissingAttributes; 097 098 /** 099 * The connection 100 */ 101 protected Connection connection; 102 103 /** 104 * Maps parameter names to arrays of ints which are the parameter indices. 105 */ 106 protected Map<String, int[]> indexMap; 107 108 /** 109 * The statement this object is wrapping. 110 **/ 111 protected StatementType statement; 112 113 /** 114 * Adds an item to the current batch. Oddly this is not implemented in {@link Statement}, 115 * but it is implemented by both sub-types of statement. 116 * 117 * @see PreparedStatement#addBatch() 118 * @see CallableStatement#addBatch() 119 */ 120 public abstract void addBatch() throws SQLException; 121 122 /** 123 * Closes the statement. 124 * 125 * @throws SQLException if an error occurred 126 * @see Statement#close() 127 */ 128 public void close() throws SQLException { 129 if (!statement.isClosed()) { 130 statement.close(); 131 } 132 } 133 134 /** 135 * Executes all of the batched statements. See {@link Statement#executeBatch()} and {@link #addBatch()} for details. 136 * 137 * @return update counts for each statement 138 * @throws SQLException if something went wrong 139 */ 140 public int[] executeBatch() throws SQLException { 141 return statement.executeBatch(); 142 } 143 144 /** 145 * Returns the indexes for a parameter. 146 * 147 * @param name parameter name 148 * @return parameter indexes 149 * @throws IllegalArgumentException if the parameter does not exist 150 */ 151 protected int[] getIndexes(String name) { 152 int[] indexes = indexMap.get(name); 153 if (indexes == null) { 154 if (allowMissingAttributes) { 155 return new int[0]; 156 } else { 157 throw new IllegalArgumentException("Parameter not found: " + name); 158 } 159 } 160 return indexes; 161 } 162 163 /** 164 * Returns the set of parameter names. 165 * @return An unmodifiable set of parameter names 166 */ 167 public Set<String> getParameterNames() { 168 return Collections.unmodifiableSet(indexMap.keySet()); 169 } 170 171 /** 172 * Returns the underlying statement. 173 * 174 * @return the statement 175 */ 176 public StatementType getStatement() { 177 return statement; 178 } 179 180 /** 181 * Returns true if the wrapped statement is closed 182 * @return True if the wrapped statement is closed 183 * @see Statement#isClosed() 184 */ 185 public boolean isClosed() throws SQLException { 186 return statement.isClosed(); 187 } 188 189 /** 190 * Sets the flag to allow an attempt to set missing attributes without throwing 191 * an exception. With this flag set to false, any attempt to invoke setString, or any 192 * other setXYZ method, will result in an exception. 193 * 194 * @param allowMissingAttributes `true` if we should not throw an exception when a parameter is unused 195 */ 196 public void setAllowMissingAttributes(boolean allowMissingAttributes) { 197 this.allowMissingAttributes = allowMissingAttributes; 198 } 199 200 /** 201 * @see Statement#setFetchDirection(int) 202 */ 203 public void setFetchDirection(int i) throws SQLException { 204 statement.setFetchDirection(i); 205 } 206 207 /** 208 * @see Statement#setFetchSize(int) 209 */ 210 public void setFetchSize(int i) throws SQLException { 211 statement.setFetchSize(i); 212 } 213 214 /** 215 * Set the max rows in the result set 216 * @param i The result row size 217 * @throws SQLException on errors 218 */ 219 public void setMaxRows(int i) throws SQLException { 220 statement.setMaxRows(i); 221 } 222 223 /** 224 * Abstract method for use by subclasses to register their own specific value types 225 * @param name The name of the argument to set 226 * @param value The value to set 227 */ 228 public abstract void setObject(String name, Object value) throws SQLException; 229 230 /** 231 * Sets all parameters to this named parameter statement in bulk from the given Map 232 * 233 * @param parameters The parameters in question 234 * @throws SQLException if any failures occur 235 */ 236 public final void setParameters(Map<String, Object> parameters) throws SQLException { 237 if (parameters != null) { 238 for (String key : parameters.keySet()) { 239 setObject(key, parameters.get(key)); 240 } 241 } 242 } 243 244 /** 245 * Sets the query timeout 246 * @param seconds The timeout for this query in seconds 247 * @throws SQLException if any errors occur 248 * @see Statement#setQueryTimeout(int) 249 */ 250 public void setQueryTimeout(int seconds) throws SQLException { 251 statement.setQueryTimeout(seconds); 252 } 253 254}