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}