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<String, Object> johnDoe = new HashMap<String, Object>(); 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<String, Object> jackSmith = new HashMap<String, Object>(); 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 >= 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}