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}