001    /*
002     * The MIT License
003     *
004     * Copyright (c) 2012-2015, Ninja Squad
005     *
006     * Permission is hereby granted, free of charge, to any person obtaining a copy
007     * of this software and associated documentation files (the "Software"), to deal
008     * in the Software without restriction, including without limitation the rights
009     * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010     * copies of the Software, and to permit persons to whom the Software is
011     * furnished to do so, subject to the following conditions:
012     *
013     * The above copyright notice and this permission notice shall be included in
014     * all copies or substantial portions of the Software.
015     *
016     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019     * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021     * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
022     * THE SOFTWARE.
023     */
024    
025    package com.ninja_squad.dbsetup.operation;
026    
027    import javax.annotation.Nonnull;
028    import javax.annotation.concurrent.Immutable;
029    import java.sql.Connection;
030    import java.sql.ParameterMetaData;
031    import java.sql.PreparedStatement;
032    import java.sql.SQLException;
033    import java.util.ArrayList;
034    import java.util.Arrays;
035    import java.util.HashMap;
036    import java.util.HashSet;
037    import java.util.Iterator;
038    import java.util.LinkedHashMap;
039    import java.util.List;
040    import java.util.Map;
041    import java.util.Set;
042    
043    import com.ninja_squad.dbsetup.bind.Binder;
044    import com.ninja_squad.dbsetup.bind.BinderConfiguration;
045    import com.ninja_squad.dbsetup.bind.Binders;
046    import com.ninja_squad.dbsetup.generator.ValueGenerator;
047    import com.ninja_squad.dbsetup.generator.ValueGenerators;
048    import com.ninja_squad.dbsetup.util.Preconditions;
049    
050    /**
051     * Operation which inserts one or several rows into a table. Example usage:
052     * <pre>
053     *   Insert insert =
054     *       Insert.into("CLIENT")
055     *             .columns("CLIENT_ID", "FIRST_NAME", "LAST_NAME", "DATE_OF_BIRTH", "CLIENT_TYPE")
056     *             .values(1L, "John", "Doe", "1975-07-19", ClientType.NORMAL)
057     *             .values(2L, "Jack", "Smith", "1969-08-22", ClientType.HIGH_PRIORITY)
058     *             .withDefaultValue("DELETED", false)
059     *             .withDefaultValue("VERSION", 1)
060     *             .withBinder(new ClientTypeBinder(), "CLIENT_TYPE")
061     *             .build();
062     * </pre>
063     *
064     * The above operation will insert two rows inside the CLIENT table. For each row, the column DELETED will be set to
065     * <code>false</code> and the column VERSION will be set to 1. For the column CLIENT_TYPE, instead of using the
066     * {@link Binder} associated to the type of the column found in the metadata of the table, a custom binder will be used.
067     * <p>
068     * Instead of specifying values as an ordered sequence which must match the sequence of column names, some might prefer
069     * passing a map of column/value associations. This makes things more verbose, but can be more readable in some cases,
070     * when the number of columns is high. This also allows not specifying any value for columns that must stay null.
071     * The map can be constructed like any other map and passed to the builder, or it can be added using a fluent builder.
072     * The following snippet:
073     *
074     * <pre>
075     *   Insert insert =
076     *       Insert.into("CLIENT")
077     *             .columns("CLIENT_ID", "FIRST_NAME", "LAST_NAME", "DATE_OF_BIRTH", "CLIENT_TYPE")
078     *             .row().column("CLIENT_ID", 1L)
079     *                   .column("FIRST_NAME", "John")
080     *                   .column("LAST_NAME", "Doe")
081     *                   .column("DATE_OF_BIRTH", "1975-07-19")
082     *                   .end()
083     *             .row().column("CLIENT_ID", 2L)
084     *                   .column("FIRST_NAME", "Jack")
085     *                   .column("LAST_NAME", "Smith")
086     *                   .end() // null date of birth, because it's not in the row
087     *             .build();
088     * </pre>
089     *
090     * is thus equivalent to:
091     *
092     * <pre>
093     *   Map&lt;String, Object&gt; johnDoe = new HashMap&lt;String, Object&gt;();
094     *   johnDoe.put("CLIENT_ID", 1L);
095     *   johnDoe.put("FIRST_NAME", "John");
096     *   johnDoe.put("LAST_NAME", "Doe");
097     *   johnDoe.put("DATE_OF_BIRTH", "1975-07-19");
098     *
099     *   Map&lt;String, Object&gt; jackSmith = new HashMap&lt;String, Object&gt;();
100     *   jackSmith.put("CLIENT_ID", 2L);
101     *   jackSmith.put("FIRST_NAME", "Jack");
102     *   jackSmith.put("LAST_NAME", "Smith");
103     *
104     *   Insert insert =
105     *       Insert.into("CLIENT")
106     *             .columns("CLIENT_ID", "FIRST_NAME", "LAST_NAME", "DATE_OF_BIRTH", "CLIENT_TYPE")
107     *             .values(johnDoe)
108     *             .values(jackSmith)
109     *             .build();
110     * </pre>
111     *
112     * When building the Insert using column/value associations, it might seem redundant to specify the set of column names
113     * before inserting the rows. Remember, though, that all the rows of an Insert are inserted using the same
114     * parameterized SQL query. We thus need a robust and easy way to know all the columns to insert for every row of the
115     * insert. To be able to spot errors easily and early, and to avoid complex rules, the rule is thus simple: the set of
116     * columns (excluding the generated ones) is specified either by columns(), or by the columns of the first row. All the
117     * subsequent rows may not have additional columns. And <code>null</code> is inserted for all the absent columns of the
118     * subsequent rows. The above example can thus be written as
119     *
120     * <pre>
121     *   Insert insert =
122     *       Insert.into("CLIENT")
123     *             .row().column("CLIENT_ID", 1L)
124     *                   .column("FIRST_NAME", "John")
125     *                   .column("LAST_NAME", "Doe")
126     *                   .column("DATE_OF_BIRTH", "1975-07-19")
127     *                   .end()
128     *             .row().column("CLIENT_ID", 2L)
129     *                   .column("FIRST_NAME", "Jack")
130     *                   .column("LAST_NAME", "Smith")
131     *                   .end() // null date of birth, because it's not in the row
132     *             .build();
133     * </pre>
134     *
135     * but the following will throw an exception, because the DATE_OF_BIRTH column is not part of the first row:
136     *
137     * <pre>
138     *   Insert insert =
139     *       Insert.into("CLIENT")
140     *             .row().column("CLIENT_ID", 2L)
141     *                   .column("FIRST_NAME", "Jack")
142     *                   .column("LAST_NAME", "Smith")
143     *                   .column("CLIENT_TYPE", ClientType.HIGH_PRIORITY)
144     *                   .end()
145     *             .row().column("CLIENT_ID", 1L)
146     *                   .column("FIRST_NAME", "John")
147     *                   .column("LAST_NAME", "Doe")
148     *                   .column("DATE_OF_BIRTH", "1975-07-19")
149     *                   .column("CLIENT_TYPE", ClientType.NORMAL)
150     *                   .end()
151     *             .build();
152     * </pre>
153     *
154     * @author JB Nizet
155     */
156    @Immutable
157    public final class Insert implements Operation {
158        private final String table;
159        private final List<String> columnNames;
160        private final Map<String, List<Object>> generatedValues;
161        private final List<List<?>> rows;
162        private final boolean metadataUsed;
163    
164        private final Map<String, Binder> binders;
165    
166        private Insert(Builder builder) {
167            this.table = builder.table;
168            this.columnNames = builder.columnNames;
169            this.rows = builder.rows;
170            this.generatedValues = generateValues(builder.valueGenerators, rows.size());
171            this.binders = builder.binders;
172            this.metadataUsed = builder.metadataUsed;
173        }
174    
175        private Map<String, List<Object>> generateValues(Map<String, ValueGenerator<?>> valueGenerators,
176                                                          int count) {
177            Map<String, List<Object>> result = new LinkedHashMap<String, List<Object>>();
178            for (Map.Entry<String, ValueGenerator<?>> entry : valueGenerators.entrySet()) {
179                result.put(entry.getKey(), generateValues(entry.getValue(), count));
180            }
181            return result;
182        }
183    
184        private List<Object> generateValues(ValueGenerator<?> valueGenerator, int count) {
185            List<Object> result = new ArrayList<Object>(count);
186            for (int i = 0; i < count; i++) {
187                result.add(valueGenerator.nextValue());
188            }
189            return result;
190        }
191    
192        /**
193         * Inserts the values and generated values in the table. Unless <code>useMetadata</code> has been set to
194         * <code>false</code>, the given configuration is used to get the appropriate binder. Nevertheless, if a binder
195         * has explicitly been associated to a given column, this binder will always be used for this column.
196         */
197        @edu.umd.cs.findbugs.annotations.SuppressWarnings(
198            value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING",
199            justification = "The point here is precisely to compose a SQL String from column names coming from the user")
200        @Override
201        public void execute(Connection connection, BinderConfiguration configuration) throws SQLException {
202            List<String> allColumnNames = new ArrayList<String>(columnNames);
203            allColumnNames.addAll(generatedValues.keySet());
204    
205            String query = generateSqlQuery(allColumnNames);
206    
207            PreparedStatement stmt = connection.prepareStatement(query);
208    
209            try {
210                Map<String, Binder> usedBinders = initializeBinders(stmt, allColumnNames, configuration);
211    
212                int rowIndex = 0;
213                for (List<?> row : rows) {
214                    int i = 0;
215                    for (Object value : row) {
216                        String columnName = columnNames.get(i);
217                        Binder binder = usedBinders.get(columnName);
218                        binder.bind(stmt, i + 1, value);
219                        i++;
220                    }
221                    for (Map.Entry<String, List<Object>> entry : generatedValues.entrySet()) {
222                        String columnName = entry.getKey();
223                        List<Object> rowValues = entry.getValue();
224                        Binder binder = usedBinders.get(columnName);
225                        binder.bind(stmt, i + 1, rowValues.get(rowIndex));
226                        i++;
227                    }
228    
229                    stmt.executeUpdate();
230                    rowIndex++;
231                }
232            }
233            finally {
234                stmt.close();
235            }
236        }
237    
238        /**
239         * Gets the number of rows that are inserted in the database table when this insert operation is executed.
240         */
241        public int getRowCount() {
242            return rows.size();
243        }
244    
245        private String generateSqlQuery(List<String> allColumnNames) {
246            StringBuilder sql = new StringBuilder("insert into ").append(table).append(" (");
247            for (Iterator<String> it = allColumnNames.iterator(); it.hasNext(); ) {
248                String columnName = it.next();
249                sql.append(columnName);
250                if (it.hasNext()) {
251                    sql.append(", ");
252                }
253            }
254            sql.append(") values (");
255            for (Iterator<String> it = allColumnNames.iterator(); it.hasNext(); ) {
256                it.next();
257                sql.append('?');
258                if (it.hasNext()) {
259                    sql.append(", ");
260                }
261            }
262            sql.append(')');
263    
264            return sql.toString();
265        }
266    
267        private Map<String, Binder> initializeBinders(PreparedStatement stmt,
268                                                      List<String> allColumnNames,
269                                                      BinderConfiguration configuration) throws SQLException {
270            Map<String, Binder> result = new HashMap<String, Binder>();
271            ParameterMetaData metadata = null;
272            if (metadataUsed) {
273                try {
274                    metadata = stmt.getParameterMetaData();
275                }
276                catch (SQLException e) {
277                    metadata = null;
278                    // the parameter metadata are probably not supported by the database. Pass null to the configuration.
279                    // The default configuration will return the default binder, just as if useMetadata(false) had been used
280                }
281            }
282            int i = 1;
283            for (String columnName : allColumnNames) {
284                Binder binder = this.binders.get(columnName);
285                if (binder == null) {
286                    binder = configuration.getBinder(metadata, i);
287                    if (binder == null) {
288                        throw new IllegalStateException("null binder returned from configuration "
289                                                        + configuration.getClass());
290                    }
291                }
292                result.put(columnName, binder);
293                i++;
294            }
295            return result;
296        }
297    
298        @Override
299        public String toString() {
300            return "insert into "
301                   + table
302                   + " [columns="
303                   + columnNames
304                   + ", generatedValues="
305                   + generatedValues
306                   + ", rows="
307                   + rows
308                   + ", metadataUsed="
309                   + metadataUsed
310                   + ", binders="
311                   + binders
312                   + "]";
313    
314        }
315    
316        @Override
317        public int hashCode() {
318            final int prime = 31;
319            int result = 1;
320            result = prime * result + binders.hashCode();
321            result = prime * result + columnNames.hashCode();
322            result = prime * result + generatedValues.hashCode();
323            result = prime * result + Boolean.valueOf(metadataUsed).hashCode();
324            result = prime * result + rows.hashCode();
325            result = prime * result + table.hashCode();
326            return result;
327        }
328    
329        @Override
330        public boolean equals(Object obj) {
331            if (this == obj) {
332                return true;
333            }
334            if (obj == null) {
335                return false;
336            }
337            if (getClass() != obj.getClass()) {
338                return false;
339            }
340            Insert other = (Insert) obj;
341    
342            return binders.equals(other.binders)
343                   && columnNames.equals(other.columnNames)
344                   && generatedValues.equals(other.generatedValues)
345                   && metadataUsed == other.metadataUsed
346                   && rows.equals(other.rows)
347                   && table.equals(other.table);
348        }
349    
350        /**
351         * Creates a new Builder instance, in order to build an Insert operation into the given table
352         * @param table the name of the table to insert into
353         * @return the created Builder
354         */
355        public static Builder into(@Nonnull String table) {
356            Preconditions.checkNotNull(table, "table may not be null");
357            return new Builder(table);
358        }
359    
360        /**
361         * A builder used to create an Insert operation. Such a builder may only be used once. Once it has built its Insert
362         * operation, all its methods throw an {@link IllegalStateException}.
363         * @see Insert
364         * @see Insert#into(String)
365         * @author JB Nizet
366         */
367        public static final class Builder {
368            private final String table;
369            private final List<String> columnNames = new ArrayList<String>();
370            private final Map<String, ValueGenerator<?>> valueGenerators = new LinkedHashMap<String, ValueGenerator<?>>();
371            private final List<List<?>> rows = new ArrayList<List<?>>();
372    
373            private boolean metadataUsed = true;
374            private final Map<String, Binder> binders = new HashMap<String, Binder>();
375    
376            private boolean built;
377    
378            private Builder(String table) {
379                this.table = table;
380            }
381    
382            /**
383             * Specifies the list of columns into which values will be inserted. The values must the be specified, after,
384             * using the {@link #values(Object...)} method, or with the {@link #values(java.util.Map)} method, or by adding
385             * a row with named columns fluently using {@link #row()}.
386             * @param columns the names of the columns to insert into.
387             * @return this Builder instance, for chaining.
388             * @throws IllegalStateException if the Insert has already been built, or if this method has already been
389             * called, or if one of the given columns is also specified as one of the generated value columns, or if the
390             * set of columns has already been defined by adding a first row to the builder.
391             */
392            public Builder columns(@Nonnull String... columns) {
393                Preconditions.checkState(!built, "The insert has already been built");
394                Preconditions.checkState(columnNames.isEmpty(), "columns have already been specified");
395                for (String column : columns) {
396                    Preconditions.checkNotNull(column, "column may not be null");
397                    Preconditions.checkState(!valueGenerators.containsKey(column),
398                                             "column "
399                                                 + column
400                                                 + " has already been specified as generated value column");
401                }
402                columnNames.addAll(Arrays.asList(columns));
403                return this;
404            }
405    
406            /**
407             * Adds a row of values to insert.
408             * @param values the values to insert.
409             * @return this Builder instance, for chaining.
410             * @throws IllegalStateException if the Insert has already been built, or if the number of values doesn't match
411             * the number of columns.
412             */
413            public Builder values(@Nonnull Object... values) {
414                return addRepeatingValues(Arrays.asList(values), 1);
415            }
416    
417            /**
418             * Allows adding many rows with the same non-generated values to insert.
419             * @param values the values to insert.
420             * @return A RowRepeater, allowing to choose how many similar rows to add.
421             * @throws IllegalStateException if the Insert has already been built, or if the number of values doesn't match
422             * the number of columns.
423             */
424            public RowRepeater repeatingValues(@Nonnull Object... values) {
425                Preconditions.checkState(!built, "The insert has already been built");
426                Preconditions.checkArgument(values.length == columnNames.size(),
427                                            "The number of values doesn't match the number of columns");
428                return new ListRowRepeater(this, Arrays.asList(values));
429            }
430    
431            /**
432             * Starts building a new row with named columns to insert. If the row is the first one being added and the
433             * columns haven't been set yet by calling <code>columns()</code>, then the columns of this row constitute the
434             * column names (excluding the generated ones) of the Insert being built
435             * @return a {@link RowBuilder} instance, which, when built, will add a row (or several ones) to this insert
436             * builder.
437             * @throws IllegalStateException if the Insert has already been built.
438             * @see RowBuilder
439             */
440            public RowBuilder row() {
441                Preconditions.checkState(!built, "The insert has already been built");
442                return new RowBuilder(this);
443            }
444    
445            /**
446             * Adds a row to this builder. If no row has been added yet and the columns haven't been set yet by calling
447             * <code>columns()</code>, then the keys of this map constitute the column names (excluding the generated ones)
448             * of the Insert being built, in the order of the keys in the map (which is arbitrary unless an ordered or
449             * sorted map is used).
450             * @param row the row to add. The keys of the map are the column names, which must match with
451             * the column names specified in the call to {@link #columns(String...)}, or with the column names of the first
452             * added row. If a column name is not present in the map, null is inserted for this column.
453             * @return this Builder instance, for chaining.
454             * @throws IllegalStateException if the Insert has already been built.
455             * @throws IllegalArgumentException if a column name of the map doesn't match with any of the column names
456             * specified with {@link #columns(String...)}
457             */
458            public Builder values(@Nonnull Map<String, ?> row) {
459                return addRepeatingValues(row, 1);
460            }
461    
462            /**
463             * Allows adding many rows with the same non-generated values to insert.
464             * @return A RowRepeater, allowing to choose how many similar rows to add.
465             * @throws IllegalStateException if the Insert has already been built.
466             * @see #values(Map)
467             */
468            public RowRepeater repeatingValues(@Nonnull Map<String, ?> row) {
469                Preconditions.checkState(!built, "The insert has already been built");
470                Preconditions.checkNotNull(row, "The row may not be null");
471                return new MapRowRepeater(this, row);
472            }
473    
474            /**
475             * Associates a Binder to one or several columns.
476             * @param binder the binder to use, regardless of the metadata, for the given columns
477             * @param columns the name of the columns to associate with the given Binder
478             * @return this Builder instance, for chaining.
479             * @throws IllegalStateException if the Insert has already been built,
480             * @throws IllegalArgumentException if any of the given columns is not
481             * part of the columns or "generated value" columns.
482             */
483            public Builder withBinder(@Nonnull Binder binder, @Nonnull String... columns) {
484                Preconditions.checkState(!built, "The insert has already been built");
485                Preconditions.checkNotNull(binder, "binder may not be null");
486                for (String columnName : columns) {
487                    Preconditions.checkArgument(this.columnNames.contains(columnName)
488                                                || this.valueGenerators.containsKey(columnName),
489                                                "column "
490                                                    + columnName
491                                                    + " is not one of the registered column names");
492                    binders.put(columnName, binder);
493                }
494                return this;
495            }
496    
497            /**
498             * Specifies a default value to be inserted in a column for all the rows inserted by the Insert operation.
499             * Calling this method is equivalent to calling
500             * <code>withGeneratedValue(column, ValueGenerators.constant(value))</code>
501             * @param column the name of the column
502             * @param value the default value to insert into the column
503             * @return this Builder instance, for chaining.
504             * @throws IllegalStateException if the Insert has already been built, or if the given column is part
505             * of the columns to insert.
506             */
507            public Builder withDefaultValue(@Nonnull String column, Object value) {
508                return withGeneratedValue(column, ValueGenerators.constant(value));
509            }
510    
511            /**
512             * Allows the given column to be populated by a value generator, which will be called for every row of the
513             * Insert operation being built.
514             * @param column the name of the column
515             * @param valueGenerator the generator generating values for the given column of every row
516             * @return this Builder instance, for chaining.
517             * @throws IllegalStateException if the Insert has already been built, or if the given column is part
518             * of the columns to insert.
519             */
520            public Builder withGeneratedValue(@Nonnull String column, @Nonnull ValueGenerator<?> valueGenerator) {
521                Preconditions.checkState(!built, "The insert has already been built");
522                Preconditions.checkNotNull(column, "column may not be null");
523                Preconditions.checkNotNull(valueGenerator, "valueGenerator may not be null");
524                Preconditions.checkArgument(!columnNames.contains(column),
525                                            "column "
526                                            + column
527                                            + " is already listed in the list of column names");
528                valueGenerators.put(column, valueGenerator);
529                return this;
530            }
531    
532            /**
533             * Determines if the metadata must be used to get the appropriate binder for each inserted column (except
534             * the ones which have been associated explicitly with a Binder). The default is <code>true</code>. The insert
535             * can be faster if set to <code>false</code>, but in this case, the binder used will be the one returned
536             * by the {@link BinderConfiguration} for a null metadata (which is, by default, the
537             * {@link Binders#defaultBinder() default binder}), except the ones which have been associated explicitly with
538             * a Binder.<br/>
539             * Before version 1.3.0, a SQLException was thrown if the database doesn't support parameter metadata and
540             * <code>useMetadata(false)</code> wasn't called. Since version 1.3.0, if <code>useMetadata</code> is true
541             * (the default) but the database doesn't support metadata, then the default binder configuration returns the
542             * default binder. Using this method is thus normally unnecessary as of 1.3.0.
543             * @return this Builder instance, for chaining.
544             * @throws IllegalStateException if the Insert has already been built.
545             */
546            public Builder useMetadata(boolean useMetadata) {
547                Preconditions.checkState(!built, "The insert has already been built");
548                this.metadataUsed = useMetadata;
549                return this;
550            }
551    
552            /**
553             * Builds the Insert operation.
554             * @return the created Insert operation.
555             * @throws IllegalStateException if the Insert has already been built, or if no column and no generated value
556             * column has been specified.
557             */
558            public Insert build() {
559                Preconditions.checkState(!built, "The insert has already been built");
560                Preconditions.checkState(!this.columnNames.isEmpty() || !this.valueGenerators.isEmpty(),
561                                         "no column and no generated value column has been specified");
562                built = true;
563                return new Insert(this);
564            }
565    
566            @Override
567            public String toString() {
568                return "insert into "
569                    + table
570                    + " [columns="
571                    + columnNames
572                    + ", rows="
573                    + rows
574                    + ", valueGenerators="
575                    + valueGenerators
576                    + ", metadataUsed="
577                    + metadataUsed
578                    + ", binders="
579                    + binders
580                    + ", built="
581                    + built
582                    + "]";
583            }
584    
585            private Builder addRepeatingValues(List<?> values, int times) {
586                Preconditions.checkState(!built, "The insert has already been built");
587                Preconditions.checkArgument(values.size() == columnNames.size(),
588                                            "The number of values doesn't match the number of columns");
589    
590                List<Object> row = new ArrayList<Object>(values);
591                for (int i = 0; i < times; i++) {
592                    rows.add(row);
593                }
594                return this;
595            }
596    
597            private Builder addRepeatingValues(@Nonnull Map<String, ?> row, int times) {
598                Preconditions.checkState(!built, "The insert has already been built");
599                Preconditions.checkNotNull(row, "The row may not be null");
600    
601                List<Object> values = mapToRow(row);
602                for (int i = 0; i < times; i++) {
603                    rows.add(values);
604                }
605                return this;
606            }
607    
608            private List<Object> mapToRow(@Nonnull Map<String, ?> row) {
609                boolean setColumns = rows.isEmpty() && columnNames.isEmpty();
610                if (setColumns) {
611                    columns(row.keySet().toArray(new String[row.size()]));
612                }
613                else {
614                    Set<String> rowColumnNames = new HashSet<String>(row.keySet());
615                    rowColumnNames.removeAll(columnNames);
616                    if (!rowColumnNames.isEmpty()) {
617                        throw new IllegalArgumentException(
618                            "The following columns of the row don't match with any column name: " + rowColumnNames);
619                    }
620                }
621    
622                List<Object> values = new ArrayList<Object>(columnNames.size());
623                for (String columnName : columnNames) {
624                    values.add(row.get(columnName));
625                }
626                return values;
627            }
628        }
629    
630        /**
631         * A row builder, constructed with {@link com.ninja_squad.dbsetup.operation.Insert.Builder#row()}. This builder
632         * allows adding a row with named columns to an Insert:
633         *
634         * <pre>
635         *   Insert insert =
636         *       Insert.into("CLIENT")
637         *             .columns("CLIENT_ID", "FIRST_NAME", "LAST_NAME", "DATE_OF_BIRTH", "CLIENT_TYPE")
638         *             .row().column("CLIENT_ID", 1L)
639         *                   .column("FIRST_NAME", "John")
640         *                   .column("LAST_NAME", "Doe")
641         *                   .column("DATE_OF_BIRTH", "1975-07-19")
642         *                   .column("CLIENT_TYPE", ClientType.NORMAL)
643         *                   .end()
644         *             .row().column("CLIENT_ID", 2L)
645         *                   .column("FIRST_NAME", "Jack")
646         *                   .column("LAST_NAME", "Smith")
647         *                   .column("DATE_OF_BIRTH", "1969-08-22")
648         *                   .column("CLIENT_TYPE", ClientType.HIGH_PRIORITY)
649         *                   .end()
650         *             .build();
651         * </pre>
652         *
653         * You may omit the call to <code>columns()</code>. In that case, the columns of the Insert will be the columns
654         * specified in the first added row.
655         */
656        public static final class RowBuilder {
657            private final Builder builder;
658            private final Map<String, Object> row;
659            private boolean ended;
660    
661            private RowBuilder(Builder builder) {
662                this.builder = builder;
663                // note: very important to use a LinkedHashMap here, to guarantee the ordering of the columns.
664                this.row = new LinkedHashMap<String, Object>();
665            }
666    
667            /**
668             * Adds a new named column to the row. If a previous value has already been added for the same column, it's
669             * replaced by this new value.
670             * @param name the name of the column, which must match with a column name defined in the Insert Builder
671             * @param value the value of the column for the constructed row
672             * @return this builder, for chaining
673             * @throws IllegalArgumentException if the given name is not the name of one of the columns to insert
674             */
675            public RowBuilder column(@Nonnull String name, Object value) {
676                Preconditions.checkState(!ended, "The row has already been ended and added to the Insert Builder");
677                if (!builder.columnNames.isEmpty()) {
678                    Preconditions.checkNotNull(name, "the column name may not be null");
679                    Preconditions.checkArgument(builder.columnNames.contains(name),
680                                                "column " + name + " is not one of the registered column names");
681                }
682                row.put(name, value);
683                return this;
684            }
685    
686            /**
687             * Ends the row, adds it to the Insert Builder and returns it, for chaining.
688             * @return the Insert Builder
689             */
690            public Builder end() {
691                Preconditions.checkState(!ended, "The row has already been ended and added to the Insert Builder");
692                ended = true;
693                return builder.values(row);
694            }
695    
696            /**
697             * Ends the row, adds it to the Insert Builder the given amount of times, and returns it, for chaining.
698             * @param times the number of rows to add. Must be >= 0. If zero, no row is added.
699             * @return the Insert Builder
700             */
701            public Builder times(int times) {
702                Preconditions.checkArgument(times >= 0, "the number of repeating values must be >= 0");
703                Preconditions.checkState(!ended, "The row has already been ended and added to the Insert Builder");
704                ended = true;
705                return builder.addRepeatingValues(row, times);
706            }
707        }
708    
709        /**
710         * Allows inserting the same list of non-generated values several times.
711         */
712        public interface RowRepeater {
713            /**
714             * Adds several rows with the same non-generated values to the insert. This method can only be called once.
715             * @param times the number of rows to add. Must be >= 0. If zero, no row is added.
716             * @return the Insert Builder, for chaining
717             * @throws IllegalStateException if the rows have already been added
718             */
719            Builder times(int times);
720        }
721    
722        /**
723         * Base abstract class for row repeaters.
724         */
725        private abstract static class AbstractRowRepeater implements RowRepeater {
726            protected final Builder builder;
727            private boolean ended;
728    
729            public AbstractRowRepeater(Builder builder) {
730                this.builder = builder;
731            }
732    
733            protected abstract Builder doTimes(int times);
734    
735            @Override
736            public Builder times(int times) {
737                Preconditions.checkArgument(times >= 0, "the number of repeating values must be >= 0");
738                Preconditions.checkState(!ended, "The rows have already been ended and added to the Insert Builder");
739                ended = true;
740                return doTimes(times);
741            }
742        }
743    
744        /**
745         * Allows inserting the same list of non-generated values as list several times.
746         */
747        private static final class ListRowRepeater extends AbstractRowRepeater {
748            private final List<Object> values;
749    
750            private ListRowRepeater(Builder builder, List<Object> values) {
751                super(builder);
752                this.values = values;
753            }
754    
755            @Override
756            public Builder doTimes(int times) {
757                return builder.addRepeatingValues(values, times);
758            }
759        }
760    
761        /**
762         * Allows inserting the same list of non-generated values as map several times.
763         */
764        private static final class MapRowRepeater extends AbstractRowRepeater {
765            private final Map<String, ?> values;
766    
767            private MapRowRepeater(Builder builder, Map<String, ?> values) {
768                super(builder);
769                this.values = values;
770            }
771    
772            @Override
773            public Builder doTimes(int times) {
774                return builder.addRepeatingValues(values, times);
775            }
776        }
777    }