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