001/*
002 * The MIT License
003 *
004 * Copyright (c) 2012-2016, 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
025package com.ninja_squad.dbsetup.operation;
026
027import javax.annotation.Nonnull;
028import javax.annotation.concurrent.Immutable;
029import java.sql.Connection;
030import java.sql.ParameterMetaData;
031import java.sql.PreparedStatement;
032import java.sql.SQLException;
033import java.util.ArrayList;
034import java.util.Arrays;
035import java.util.HashMap;
036import java.util.HashSet;
037import java.util.Iterator;
038import java.util.LinkedHashMap;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042
043import com.ninja_squad.dbsetup.bind.Binder;
044import com.ninja_squad.dbsetup.bind.BinderConfiguration;
045import com.ninja_squad.dbsetup.bind.Binders;
046import com.ninja_squad.dbsetup.generator.ValueGenerator;
047import com.ninja_squad.dbsetup.generator.ValueGenerators;
048import 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
157public 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 &gt;= 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 &gt;= 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}