001package com.identityworksllc.iiq.common.query;
002
003import sailpoint.object.Attributes;
004import sailpoint.object.SailPointObject;
005import sailpoint.tools.JdbcUtil;
006import sailpoint.tools.xml.AbstractXmlObject;
007import sailpoint.tools.xml.XMLObjectFactory;
008
009import java.io.ByteArrayInputStream;
010import java.io.IOException;
011import java.io.Reader;
012import java.sql.*;
013import java.time.Instant;
014import java.time.LocalDateTime;
015import java.time.OffsetDateTime;
016import java.util.HashMap;
017import java.util.List;
018
019/**
020 * Named parameter prepared statement wrapper, derived from various examples
021 * around the Internet. Rather than setting arguments by position, you can
022 * set them by name, the way Hibernate or certain JDBC drivers permit.
023 *
024 * Tokens have the form `:name` and must be alphanumeric.
025 *
026 * The same tokens can be re-used more than once in your query.
027 *
028 * ```
029 * NamedParameterStatement stmt = new NamedParameterStatement(
030 *        connection,
031 *        "select id from spt_identity where (extended3 = :username or name = :username) and extended4 = :department"
032 * );
033 *
034 * stmt.setString("username", someUsername);
035 * stmt.setString("department", someDepartment);
036 *
037 * // Use the statement like any other
038 * ```
039 *
040 * If you attempt to set a token that does not exist, such as `stmt.setString("xyz", str)` in the
041 * above example, the default behavior is to throw an {@link IllegalArgumentException}. You can use
042 * {@link #setAllowMissingAttributes(boolean)} to disable this behavior.
043 *
044 */
045@SuppressWarnings("unused")
046public final class NamedParameterStatement extends AbstractNamedParameterStatement<PreparedStatement> {
047
048        /**
049         * Creates a NamedParameterStatement. Wraps a call to {@link Connection#prepareStatement(java.lang.String)}.
050         * 
051         * @param connection the database connection
052         * @param query the parameterized query
053         * @throws SQLException if the statement could not be created
054         */
055        public NamedParameterStatement(Connection connection, String query) throws SQLException {
056                indexMap = new HashMap<>();
057                String parsedQuery = parse(query, indexMap);
058                this.connection = connection;
059                this.statement = connection.prepareStatement(parsedQuery);
060                this.allowMissingAttributes = false;
061        }
062
063        /**
064         * Adds the current row to the batch
065         * @throws SQLException The batch
066         */
067        public void addBatch() throws SQLException {
068                this.statement.addBatch();
069        }
070
071        /**
072         * Executes the statement.
073         *
074         * @return true if the first result is a {@link ResultSet}
075         * @throws SQLException if an error occurred
076         * @see PreparedStatement#execute()
077         */
078        public boolean execute() throws SQLException {
079                return statement.execute();
080        }
081
082        /**
083         * Executes the statement and returns a result set
084         * @return The result set
085         * @throws SQLException on errors
086         */
087        public ResultSet executeQuery() throws SQLException {
088                return statement.executeQuery();
089        }
090
091        /**
092         * Executes the statement and returns the update count
093         * @return The update count
094         * @throws SQLException on errors
095         */
096        public int executeUpdate() throws SQLException {
097                return statement.executeUpdate();
098        }
099
100        /**
101         * Sets a parameter to an array. Note that this is not supported in all database engines (particularly, not in MySQL)
102         * @param name The name of the field
103         * @param value The list to change to an array
104         * @throws SQLException if a failure generating the array occurs
105         */
106        public void setArray(String name, List<String> value) throws SQLException {
107                if (value == null) {
108                        setNull(name, Types.ARRAY);
109                } else {
110                        int[] indexes = getIndexes(name);
111                        for (int idx : indexes) {
112                                Array array = connection.createArrayOf("VARCHAR", value.toArray());
113                                statement.setArray(idx, array);
114                        }
115                }
116        }
117
118        /**
119         * Sets the given byte array as a Blob input to the statement
120         * @param name The parameter name
121         * @param blob The blob
122         * @throws SQLException on failures
123         */
124        public void setBlob(String name, Blob blob) throws SQLException {
125                if (blob == null) {
126                        setNull(name, Types.BLOB);
127                } else {
128                        int[] indexes = getIndexes(name);
129                        for (int idx : indexes) {
130                                statement.setBlob(idx, blob);
131                        }
132                }
133        }
134
135        /**
136         * Sets the given byte array as a Blob input to the statement
137         * @param name The parameter name
138         * @param blob The blob
139         * @throws SQLException on failures setting the blob or reading the byte array
140         */
141        public void setBlob(String name, byte[] blob) throws SQLException {
142                if (blob == null) {
143                        setNull(name, Types.BLOB);
144                } else {
145                        int[] indexes = getIndexes(name);
146                        for (int idx : indexes) {
147                                try (ByteArrayInputStream bais = new ByteArrayInputStream(blob)) {
148                                        statement.setBlob(idx, bais);
149                                } catch (IOException e) {
150                                        throw new SQLException(e);
151                                }
152                        }
153                }
154        }
155
156        /**
157         * Sets a parameter to a clob
158         * @param name The named parameter
159         * @param clob The clob to set
160         * @throws SQLException on failures
161         */
162        public void setClob(String name, Reader clob) throws SQLException {
163                if (clob == null) {
164                        setNull(name, Types.CLOB);
165                } else {
166                        int[] indexes = getIndexes(name);
167                        for (int idx : indexes) {
168                                // What does this do if there is more than one? Readers
169                                // can't usually be read twice. Do we want to copy the Reader?
170                                statement.setClob(idx, clob);
171                        }
172                }
173        }
174
175        /**
176         * Sets a parameter to a clob
177         * @param name The named parameter
178         * @param clob The clob to set
179         * @throws SQLException on failures
180         */
181        public void setClob(String name, String clob) throws SQLException {
182                if (clob == null) {
183                        setNull(name, Types.CLOB);
184                } else {
185                        int[] indexes = getIndexes(name);
186                        for (int idx : indexes) {
187                                if (JdbcUtil.isOracle(statement)) {
188                                        JdbcUtil.setOracleCLOBParameter(statement, idx, clob);
189                                } else {
190                                        JdbcUtil.setClobParameter(statement, idx, clob);
191                                }
192                        }
193                }
194        }
195
196
197        /**
198         * Sets a parameter to a clob
199         * @param name The named parameter
200         * @param clob The clob to set
201         * @throws SQLException on failures
202         */
203        public void setClob(String name, Clob clob) throws SQLException {
204                if (clob == null) {
205                        setNull(name, Types.CLOB);
206                } else {
207                        int[] indexes = getIndexes(name);
208                        for (int idx : indexes) {
209                                statement.setClob(idx, clob);
210                        }
211                }
212        }
213
214        /**
215         * Sets a parameter to a Java time instant.
216         *
217         * @param name parameter name
218         * @param value parameter value
219         * @throws SQLException if an error occurred
220         * @throws IllegalArgumentException if the parameter does not exist
221         * @see PreparedStatement#setDate(int, Date)
222         */
223        public void setDate(String name, LocalDateTime value) throws SQLException {
224                if (value == null) {
225                        setNull(name, Types.DATE);
226                } else {
227                        Date localDate = new Date(value.toEpochSecond(OffsetDateTime.now().getOffset()));
228                        setDate(name, localDate);
229                }
230        }
231
232
233        /**
234         * Sets a parameter to a Java time instant.
235         *
236         * @param name parameter name
237         * @param value parameter value
238         * @throws SQLException if an error occurred
239         * @throws IllegalArgumentException if the parameter does not exist
240         * @see PreparedStatement#setDate(int, Date)
241         */
242        public void setDate(String name, Instant value) throws SQLException {
243                if (value == null) {
244                        setNull(name, Types.DATE);
245                } else {
246                        setDate(name, new Date(value.toEpochMilli()));
247                }
248        }
249
250        /**
251         * Sets a parameter to a Java date.
252         *
253         * @param name parameter name
254         * @param value parameter value
255         * @throws SQLException if an error occurred
256         * @throws IllegalArgumentException if the parameter does not exist
257         * @see PreparedStatement#setDate(int, Date)
258         */
259        public void setDate(String name, java.util.Date value) throws SQLException {
260                if (value == null) {
261                        setNull(name, Types.DATE);
262                } else {
263                        java.sql.Date sqlDate = new java.sql.Date(value.getTime());
264                        int[] indexes = getIndexes(name);
265                        for (int idx : indexes) {
266                                statement.setDate(idx, sqlDate);
267                        }
268                }
269        }
270
271        /**
272         * Sets a parameter.
273         *
274         * @param name parameter name
275         * @param value parameter value
276         * @throws SQLException if an error occurred
277         * @throws IllegalArgumentException if the parameter does not exist
278         * @see PreparedStatement#setDate(int, Date)
279         */
280        public void setDate(String name, Date value) throws SQLException {
281                if (value == null) {
282                        setNull(name, Types.DATE);
283                } else {
284                        int[] indexes = getIndexes(name);
285                        for (int idx : indexes) {
286                                statement.setDate(idx, value);
287                        }
288                }
289        }
290
291        /**
292         * Sets a parameter.
293         *
294         * @param name parameter name
295         * @param value parameter value
296         * @throws SQLException if an error occurred
297         * @throws IllegalArgumentException if the parameter does not exist
298         * @see PreparedStatement#setDouble(int, double)
299         */
300        public void setDouble(String name, double value) throws SQLException {
301                int[] indexes = getIndexes(name);
302                for (int idx : indexes) {
303                        statement.setDouble(idx, value);
304                }
305        }
306
307        /**
308         * Sets a parameter.
309         *
310         * @param name parameter name
311         * @param value parameter value
312         * @throws SQLException if an error occurred
313         * @throws IllegalArgumentException if the parameter does not exist
314         * @see PreparedStatement#setInt(int, int)
315         */
316        public void setInt(String name, int value) throws SQLException {
317                int[] indexes = getIndexes(name);
318                for (int idx : indexes) {
319                        statement.setInt(idx, value);
320                }
321        }
322
323        /**
324         * Sets a parameter.
325         *
326         * @param name parameter name
327         * @param value parameter value
328         * @throws SQLException if an error occurred
329         * @throws IllegalArgumentException if the parameter does not exist
330         * @see PreparedStatement#setLong(int, long)
331         */
332        public void setLong(String name, long value) throws SQLException {
333                int[] indexes = getIndexes(name);
334                for (int idx : indexes) {
335                        statement.setLong(idx, value);
336                }
337        }
338
339        /**
340         * Sets a parameter to null, assuming the type to be VARCHAR.
341         *
342         * @param name parameter name
343         * @throws SQLException if an error occurred
344         * @throws IllegalArgumentException if the parameter does not exist
345         * @see PreparedStatement#setNull(int, int)
346         */
347        public void setNull(String name) throws SQLException {
348                setNull(name, Types.VARCHAR);
349        }
350
351        /**
352         * Sets a typed parameter to null
353         *
354         * @param name parameter name
355         * @param type The SQL type of the argument to set
356         * @throws SQLException if an error occurred
357         * @throws IllegalArgumentException if the parameter does not exist
358         * @see PreparedStatement#setNull(int, int)
359         */
360        public void setNull(String name, int type) throws SQLException {
361                int[] indexes = getIndexes(name);
362                for (int idx : indexes) {
363                        statement.setNull(idx, type);
364                }
365        }
366
367        /**
368         * Sets a parameter to the given object. If the object is a SailPointObject,
369         * the parameter will be set as to its 'id' as a string instead.
370         *
371         * @param name parameter name
372         * @param value parameter value
373         * @throws SQLException if an error occurred
374         * @throws IllegalArgumentException if the parameter does not exist
375         * @see PreparedStatement#setObject(int, java.lang.Object)
376         */
377        public void setObject(String name, Object value) throws SQLException {
378                if (value == null) {
379                        setNull(name, Types.JAVA_OBJECT);
380                } else if (value instanceof Attributes) {
381                        XMLObjectFactory f = XMLObjectFactory.getInstance();
382                        String xml = f.toXmlNoIndent(value);
383                        setClob(name, xml);
384                } else if (value instanceof SailPointObject) {
385                        setString(name, ((SailPointObject) value).getId());
386                } else if (value instanceof String) {
387                        setString(name, (String) value);
388                } else if (value instanceof Timestamp) {
389                        setTimestamp(name, (Timestamp)value);
390                } else if (value instanceof java.util.Date) {
391                        setDate(name, (java.util.Date) value);
392                } else if (value instanceof Integer) {
393                        setInt(name, (Integer)value);
394                } else if (value instanceof Long) {
395                        setLong(name, (Long)value);
396                } else {
397                        int[] indexes = getIndexes(name);
398                        for (int idx : indexes) {
399                                statement.setObject(idx, value);
400                        }
401                }
402        }
403
404        /**
405         * Sets a parameter.
406         *
407         * @param name parameter name
408         * @param value parameter value
409         * @throws SQLException if an error occurred
410         * @throws IllegalArgumentException if the parameter does not exist
411         * @see PreparedStatement#setString(int, java.lang.String)
412         */
413        public void setString(String name, String value) throws SQLException {
414                if (value == null) {
415                        setNull(name);
416                } else {
417                        int[] indexes = getIndexes(name);
418                        for (int idx : indexes) {
419                                statement.setString(idx, value);
420                        }
421                }
422        }
423
424        /**
425         * Sets a parameter.
426         *
427         * @param name parameter name
428         * @param value parameter value
429         * @throws SQLException if an error occurred
430         * @throws IllegalArgumentException if the parameter does not exist
431         * @see PreparedStatement#setTimestamp(int, java.sql.Timestamp)
432         */
433        public void setTimestamp(String name, Timestamp value) throws SQLException {
434                if (value == null) {
435                        setNull(name, Types.TIMESTAMP);
436                } else {
437                        int[] indexes = getIndexes(name);
438                        for (int idx : indexes) {
439                                statement.setTimestamp(idx, value);
440                        }
441                }
442        }
443
444        /**
445         * Serializes the input object into XML and then adds it to the query
446         * as a CLOB.
447         *
448         * @param name The field name
449         * @param xmlObject The XML object
450         * @throws SQLException if any failures occur
451         */
452        public void setXml(String name, Object xmlObject) throws SQLException {
453                XMLObjectFactory f = XMLObjectFactory.getInstance();
454                String xml = f.toXmlNoIndent(xmlObject);
455
456                setClob(name, xml);
457        }
458}