001 /* 002 * The MIT License 003 * 004 * Copyright (c) 2012-2013, 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 com.ninja_squad.dbsetup.bind.Binder; 028 import com.ninja_squad.dbsetup.bind.BinderConfiguration; 029 import com.ninja_squad.dbsetup.bind.Binders; 030 import com.ninja_squad.dbsetup.generator.ValueGenerator; 031 import com.ninja_squad.dbsetup.generator.ValueGenerators; 032 import com.ninja_squad.dbsetup.util.Preconditions; 033 034 import javax.annotation.Nonnull; 035 import javax.annotation.concurrent.Immutable; 036 import java.sql.Connection; 037 import java.sql.ParameterMetaData; 038 import java.sql.PreparedStatement; 039 import java.sql.SQLException; 040 import java.util.ArrayList; 041 import java.util.Arrays; 042 import java.util.HashMap; 043 import java.util.Iterator; 044 import java.util.LinkedHashMap; 045 import java.util.List; 046 import java.util.Map; 047 048 /** 049 * Operation which inserts one or several rows into a table. Example usage: 050 * <pre> 051 * Insert insert = 052 * Insert.into("CLIENT") 053 * .columns("CLIENT_ID", "FIRST_NAME", "LAST_NAME", "DATE_OF_BIRTH", "CLIENT_TYPE") 054 * .values(1L, "John", "Doe", "1975-07-19", ClientType.NORMAL) 055 * .values(2L, "Jack", "Smith", "1969-08-22", ClientType.HIGH_PRIORITY) 056 * .withDefaultValue("DELETED", false) 057 * .withDefaultValue("VERSION", 1) 058 * .withBinder(new ClientTypeBinder(), "CLIENT_TYPE") 059 * .build(); 060 * </pre> 061 * 062 * The above operation will insert two rows inside the CLIENT table. For each row, the column DELETED will be set to 063 * <code>false</code> and the column VERSION will be set to 1. For the column CLIENT_TYPE, instead of using the 064 * {@link Binder} associated to the type of the column found in the metadata of the table, a custom binder will be used. 065 * 066 * @author JB Nizet 067 */ 068 @Immutable 069 public final class Insert implements Operation { 070 private final String table; 071 private final List<String> columnNames; 072 private final Map<String, List<Object>> generatedValues; 073 private final List<List<?>> rows; 074 private final boolean metadataUsed; 075 076 private final Map<String, Binder> binders; 077 078 private Insert(Builder builder) { 079 this.table = builder.table; 080 this.columnNames = builder.columnNames; 081 this.rows = builder.rows; 082 this.generatedValues = generateValues(builder.valueGenerators, rows.size()); 083 this.binders = builder.binders; 084 this.metadataUsed = builder.metadataUsed; 085 } 086 087 private Map<String, List<Object>> generateValues(Map<String, ValueGenerator<?>> valueGenerators, 088 int count) { 089 Map<String, List<Object>> result = new LinkedHashMap<String, List<Object>>(); 090 for (Map.Entry<String, ValueGenerator<?>> entry : valueGenerators.entrySet()) { 091 result.put(entry.getKey(), generateValues(entry.getValue(), count)); 092 } 093 return result; 094 } 095 096 private List<Object> generateValues(ValueGenerator<?> valueGenerator, int count) { 097 List<Object> result = new ArrayList<Object>(count); 098 for (int i = 0; i < count; i++) { 099 result.add(valueGenerator.nextValue()); 100 } 101 return result; 102 } 103 104 /** 105 * Inserts the values and generated values in the table. Unless <code>useMetadata</code> has been set to 106 * <code>false</code>, the given configuration is used to get the appropriate binder. Nevertheless, if a binder 107 * has explicitely been associated to a given column, this binder will always be used for this column. 108 */ 109 @edu.umd.cs.findbugs.annotations.SuppressWarnings( 110 value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING", 111 justification = "The point here is precisely to compose a SQL String from column names coming from the user") 112 @Override 113 public void execute(Connection connection, BinderConfiguration configuration) throws SQLException { 114 StringBuilder sql = new StringBuilder("insert into ").append(table).append(" ("); 115 116 List<String> allColumnNames = new ArrayList<String>(columnNames); 117 allColumnNames.addAll(generatedValues.keySet()); 118 119 for (Iterator<String> it = allColumnNames.iterator(); it.hasNext(); ) { 120 String columnName = it.next(); 121 sql.append(columnName); 122 if (it.hasNext()) { 123 sql.append(", "); 124 } 125 } 126 sql.append(") values ("); 127 for (Iterator<String> it = allColumnNames.iterator(); it.hasNext(); ) { 128 it.next(); 129 sql.append("?"); 130 if (it.hasNext()) { 131 sql.append(", "); 132 } 133 } 134 sql.append(")"); 135 136 PreparedStatement stmt = connection.prepareStatement(sql.toString()); 137 138 try { 139 Map<String, Binder> metadataBinders = new HashMap<String, Binder>(); 140 if (metadataUsed) { 141 initializeBinders(stmt, allColumnNames, configuration, metadataBinders); 142 } 143 144 int rowIndex = 0; 145 for (List<?> row : rows) { 146 int i = 0; 147 for (Object value : row) { 148 String columnName = columnNames.get(i); 149 Binder binder = getBinder(columnName, metadataBinders); 150 binder.bind(stmt, i + 1, value); 151 i++; 152 } 153 for (Map.Entry<String, List<Object>> entry : generatedValues.entrySet()) { 154 String columnName = entry.getKey(); 155 List<Object> rowValues = entry.getValue(); 156 Binder binder = getBinder(columnName, metadataBinders); 157 binder.bind(stmt, i + 1, rowValues.get(rowIndex)); 158 i++; 159 } 160 161 stmt.executeUpdate(); 162 rowIndex++; 163 } 164 } 165 finally { 166 stmt.close(); 167 } 168 } 169 170 private void initializeBinders(PreparedStatement stmt, 171 List<String> allColumnNames, 172 BinderConfiguration configuration, 173 Map<String, Binder> metadataBinders) throws SQLException { 174 ParameterMetaData metadata = stmt.getParameterMetaData(); 175 int i = 1; 176 for (String columnName : allColumnNames) { 177 if (!this.binders.containsKey(columnName)) { 178 metadataBinders.put(columnName, configuration.getBinder(metadata, i)); 179 } 180 i++; 181 } 182 } 183 184 private Binder getBinder(String columnName, Map<String, Binder> metadataBinders) { 185 Binder result = binders.get(columnName); 186 if (result == null) { 187 result = metadataBinders.get(columnName); 188 } 189 if (result == null) { 190 result = Binders.defaultBinder(); 191 } 192 return result; 193 } 194 195 @Override 196 public String toString() { 197 return "insert into " 198 + table 199 + " [columns=" 200 + columnNames 201 + ", generatedValues=" 202 + generatedValues 203 + ", rows=" 204 + rows 205 + ", metadataUsed=" 206 + metadataUsed 207 + ", binders=" 208 + binders 209 + "]"; 210 211 } 212 213 @Override 214 public int hashCode() { 215 final int prime = 31; 216 int result = 1; 217 result = prime * result + binders.hashCode(); 218 result = prime * result + columnNames.hashCode(); 219 result = prime * result + generatedValues.hashCode(); 220 result = prime * result + Boolean.valueOf(metadataUsed).hashCode(); 221 result = prime * result + rows.hashCode(); 222 result = prime * result + table.hashCode(); 223 return result; 224 } 225 226 @Override 227 public boolean equals(Object obj) { 228 if (this == obj) { 229 return true; 230 } 231 if (obj == null) { 232 return false; 233 } 234 if (getClass() != obj.getClass()) { 235 return false; 236 } 237 Insert other = (Insert) obj; 238 239 return binders.equals(other.binders) 240 && columnNames.equals(other.columnNames) 241 && generatedValues.equals(other.generatedValues) 242 && metadataUsed == other.metadataUsed 243 && rows.equals(other.rows) 244 && table.equals(other.table); 245 } 246 247 /** 248 * Creates a new Builder instance, in order to build an Insert operation into the given table 249 * @param table the name of the table to insert into 250 * @return the created Builder 251 */ 252 public static Builder into(@Nonnull String table) { 253 Preconditions.checkNotNull(table, "table may not be null"); 254 return new Builder(table); 255 } 256 257 /** 258 * A builder used to create an Insert operation. Such a builder may only be used once. Once it has built its Insert 259 * operation, all its methods throw an {@link IllegalStateException}. 260 * @see Insert#into(String) 261 * @author JB Nizet 262 */ 263 public static final class Builder { 264 private final String table; 265 private final List<String> columnNames = new ArrayList<String>(); 266 private final Map<String, ValueGenerator<?>> valueGenerators = new LinkedHashMap<String, ValueGenerator<?>>(); 267 private final List<List<?>> rows = new ArrayList<List<?>>(); 268 269 private boolean metadataUsed = true; 270 private final Map<String, Binder> binders = new HashMap<String, Binder>(); 271 272 private boolean built; 273 274 private Builder(String table) { 275 this.table = table; 276 } 277 278 /** 279 * Specifies the list of columns into which values wil be inserted. The values must the be specified, after, 280 * using the {@link #values(Object...)} method. 281 * @param columns the names of the columns to insert into. 282 * @return this Builder instance, for chaining. 283 * @throws IllegalStateException if the Insert has already been built, or if this method has already been 284 * called, or if one of the given columns is also specified as one of the generated value columns. 285 */ 286 public Builder columns(@Nonnull String... columns) { 287 Preconditions.checkState(!built, "The insert has already been built"); 288 Preconditions.checkState(columnNames.isEmpty(), "columns have already been specified"); 289 for (String column : columns) { 290 Preconditions.checkNotNull(column, "column may not be null"); 291 Preconditions.checkState(!valueGenerators.containsKey(column), 292 "column " 293 + column 294 + " has already been specified as generated value column"); 295 } 296 columnNames.addAll(Arrays.asList(columns)); 297 return this; 298 } 299 300 /** 301 * Adds a row of values to insert. 302 * @param values the values to insert. 303 * @return this Builder instance, for chaining. 304 * @throws IllegalStateException if the Insert has already been built, or if the number of values doesn't match 305 * the number of columns. 306 */ 307 public Builder values(@Nonnull Object... values) { 308 Preconditions.checkState(!built, "The insert has already been built"); 309 Preconditions.checkArgument(values.length == columnNames.size(), 310 "The number of values doesn't match the number of columns"); 311 rows.add(new ArrayList<Object>(Arrays.asList(values))); 312 return this; 313 } 314 315 /** 316 * Associates a Binder to one or several columns. 317 * @param binder the binder to use, regardless of the metadata, for the given columns 318 * @param columns the name of the columns to associate with the given Binder 319 * @return this Builder instance, for chaining. 320 * @throws IllegalStateException if the Insert has already been built, or if any of the given columns is not 321 * part of the columns or "generated value" columns. 322 */ 323 public Builder withBinder(@Nonnull Binder binder, @Nonnull String... columns) { 324 Preconditions.checkState(!built, "The insert has already been built"); 325 Preconditions.checkNotNull(binder, "binder may not be null"); 326 for (String columnName : columns) { 327 Preconditions.checkArgument(this.columnNames.contains(columnName) 328 || this.valueGenerators.containsKey(columnName), 329 "column " 330 + columnName 331 + " is not one of the registered column names"); 332 binders.put(columnName, binder); 333 } 334 return this; 335 } 336 337 /** 338 * Specifies a default value to be inserted in a column for all the rows inserted by the Insert operation. 339 * Calling this method is equivalent to calling 340 * <code>withGeneratedValue(column, ValueGenerators.constant(value))</code> 341 * @param column the name of the column 342 * @param value the default value to insert into the column 343 * @return this Builder instance, for chaining. 344 * @throws IllegalStateException if the Insert has already been built, or if the given column is part 345 * of the columns to insert. 346 */ 347 public Builder withDefaultValue(@Nonnull String column, Object value) { 348 return withGeneratedValue(column, ValueGenerators.constant(value)); 349 } 350 351 /** 352 * Allows the given column to be populated by a value generator, which will be called for every row of the 353 * Insert operation being built. 354 * @param column the name of the column 355 * @param valueGenerator the generator generating values for the given column of every row 356 * @return this Builder instance, for chaining. 357 * @throws IllegalStateException if the Insert has already been built, or if the given column is part 358 * of the columns to insert. 359 */ 360 public Builder withGeneratedValue(@Nonnull String column, @Nonnull ValueGenerator<?> valueGenerator) { 361 Preconditions.checkState(!built, "The insert has already been built"); 362 Preconditions.checkNotNull(column, "column may not be null"); 363 Preconditions.checkNotNull(valueGenerator, "valueGenerator may not be null"); 364 Preconditions.checkArgument(!columnNames.contains(column), 365 "column " 366 + column 367 + " is already listed in the list of column names"); 368 valueGenerators.put(column, valueGenerator); 369 return this; 370 } 371 372 /** 373 * Determines if the metadata must be used to get the appropriate binder for each inserted column (except 374 * the ones which have been associated explicitely with a Binder). The default is <code>true</code>. The insert 375 * can be faster if set to <code>false</code>, but in this case, the {@link Binders#defaultBinder() default 376 * binder} will be used for all the columns (except the ones which have been associated explicitely with a 377 * Binder). 378 * @return this Builder instance, for chaining. 379 * @throws IllegalStateException if the Insert has already been built. 380 */ 381 public Builder useMetadata(boolean useMetadata) { 382 Preconditions.checkState(!built, "The insert has already been built"); 383 this.metadataUsed = useMetadata; 384 return this; 385 } 386 387 /** 388 * Builds the Insert operation. 389 * @return the created Insert operation. 390 * @throws IllegalStateException if the Insert has already been built, or if no column and no generated value 391 * column has been specified. 392 */ 393 public Insert build() { 394 Preconditions.checkState(!built, "The insert has already been built"); 395 Preconditions.checkState(!this.columnNames.isEmpty() || !this.valueGenerators.isEmpty(), 396 "no column and no generated value column has been specified"); 397 built = true; 398 return new Insert(this); 399 } 400 } 401 }