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@SuppressWarnings("unused") 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 protected boolean allowMissingAttributes; 089 090 /** 091 * The connection 092 */ 093 protected Connection connection; 094 095 /** 096 * Maps parameter names to arrays of ints which are the parameter indices. 097 */ 098 protected Map<String, int[]> indexMap; 099 100 /** 101 * The statement this object is wrapping. 102 **/ 103 protected StatementType statement; 104 105 /** 106 * Adds an item to the current batch. Oddly this is not implemented in {@link Statement}, 107 * but it is implemented by both sub-types of statement. 108 * 109 * @see PreparedStatement#addBatch() 110 * @see CallableStatement#addBatch() 111 */ 112 public abstract void addBatch() throws SQLException; 113 114 /** 115 * Closes the statement. 116 * 117 * @throws SQLException if an error occurred 118 * @see Statement#close() 119 */ 120 public void close() throws SQLException { 121 if (!statement.isClosed()) { 122 statement.close(); 123 } 124 } 125 126 /** 127 * Executes all of the batched statements. See {@link Statement#executeBatch()} and {@link #addBatch()} for details. 128 * 129 * @return update counts for each statement 130 * @throws SQLException if something went wrong 131 */ 132 public int[] executeBatch() throws SQLException { 133 return statement.executeBatch(); 134 } 135 136 /** 137 * Returns the indexes for a parameter. 138 * 139 * @param name parameter name 140 * @return parameter indexes 141 * @throws IllegalArgumentException if the parameter does not exist 142 */ 143 protected int[] getIndexes(String name) { 144 int[] indexes = indexMap.get(name); 145 if (indexes == null) { 146 if (allowMissingAttributes) { 147 return new int[0]; 148 } else { 149 throw new IllegalArgumentException("Parameter not found: " + name); 150 } 151 } 152 return indexes; 153 } 154 155 /** 156 * Returns the set of parameter names. 157 * @return An unmodifiable set of parameter names 158 */ 159 public Set<String> getParameterNames() { 160 return Collections.unmodifiableSet(indexMap.keySet()); 161 } 162 163 /** 164 * Returns the underlying statement. 165 * 166 * @return the statement 167 */ 168 public StatementType getStatement() { 169 return statement; 170 } 171 172 /** 173 * Returns true if the wrapped statement is closed 174 * @return True if the wrapped statement is closed 175 * @see Statement#isClosed() 176 */ 177 public boolean isClosed() throws SQLException { 178 return statement.isClosed(); 179 } 180 181 /** 182 * Sets the flag to allow missing attributes without throwing an exception 183 * @param allowMissingAttributes True if we should not throw an exception when a parameter is unused 184 */ 185 public void setAllowMissingAttributes(boolean allowMissingAttributes) { 186 this.allowMissingAttributes = allowMissingAttributes; 187 } 188 189 /** 190 * @see Statement#setFetchDirection(int) 191 */ 192 public void setFetchDirection(int i) throws SQLException { 193 statement.setFetchDirection(i); 194 } 195 196 /** 197 * @see Statement#setFetchSize(int) 198 */ 199 public void setFetchSize(int i) throws SQLException { 200 statement.setFetchSize(i); 201 } 202 203 /** 204 * Set the max rows in the result set 205 * @param i The result row size 206 * @throws SQLException on errors 207 */ 208 public void setMaxRows(int i) throws SQLException { 209 statement.setMaxRows(i); 210 } 211 212 /** 213 * Abstract method for use by subclasses to register their own specific value types 214 * @param name The name of the argument to set 215 * @param value The value to set 216 */ 217 public abstract void setObject(String name, Object value) throws SQLException; 218 219 /** 220 * Sets all parameters to this named parameter statement in bulk from the given Map 221 * 222 * @param parameters The parameters in question 223 * @throws SQLException if any failures occur 224 */ 225 public final void setParameters(Map<String, Object> parameters) throws SQLException { 226 if (parameters != null) { 227 for (String key : parameters.keySet()) { 228 setObject(key, parameters.get(key)); 229 } 230 } 231 } 232 233 /** 234 * Sets the query timeout 235 * @param seconds The timeout for this query in seconds 236 * @throws SQLException if any errors occur 237 * @see Statement#setQueryTimeout(int) 238 */ 239 public void setQueryTimeout(int seconds) throws SQLException { 240 statement.setQueryTimeout(seconds); 241 } 242 243}