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}