001package org.andromda.maven.plugin.andromdapp; 002 003import java.io.BufferedReader; 004import java.io.File; 005import java.io.InputStream; 006import java.io.InputStreamReader; 007import java.lang.reflect.Field; 008import java.net.MalformedURLException; 009import java.net.URL; 010import java.net.URLClassLoader; 011import java.sql.Connection; 012import java.sql.Driver; 013import java.sql.DriverManager; 014import java.sql.SQLException; 015import java.sql.Statement; 016import java.util.ArrayList; 017import java.util.Arrays; 018import java.util.HashMap; 019import java.util.LinkedHashMap; 020import java.util.LinkedHashSet; 021import java.util.List; 022import java.util.Map; 023import java.util.Properties; 024import java.util.Set; 025import org.andromda.core.common.AndroMDALogger; 026import org.andromda.core.common.ClassUtils; 027import org.andromda.core.common.ResourceUtils; 028import org.andromda.maven.plugin.andromdapp.hibernate.HibernateCreateSchema; 029import org.andromda.maven.plugin.andromdapp.hibernate.HibernateDropSchema; 030import org.andromda.maven.plugin.andromdapp.hibernate.HibernateUpdateSchema; 031import org.andromda.maven.plugin.andromdapp.hibernate.HibernateValidateSchema; 032import org.apache.commons.lang.ObjectUtils; 033import org.apache.commons.lang.StringUtils; 034import org.apache.maven.artifact.Artifact; 035import org.apache.maven.artifact.factory.ArtifactFactory; 036import org.apache.maven.artifact.repository.ArtifactRepository; 037import org.apache.maven.artifact.resolver.ArtifactNotFoundException; 038import org.apache.maven.artifact.resolver.ArtifactResolutionException; 039import org.apache.maven.artifact.resolver.ArtifactResolver; 040import org.apache.maven.model.Dependency; 041import org.apache.maven.plugin.AbstractMojo; 042import org.apache.maven.plugin.MojoExecutionException; 043import org.apache.maven.plugin.MojoFailureException; 044import org.apache.maven.project.MavenProject; 045 046/** 047 * Provides the ability to drop database schemas. 048 * 049 * @goal schema 050 * @requiresDependencyResolution runtime 051 * @author Chad Brandon 052 */ 053public class SchemaMojo 054 extends AbstractMojo 055{ 056 /** 057 * The schema task to execute (create, drop, update, validate) 058 * 059 * @parameter expression="${tasks}" 060 */ 061 private String tasks; 062 063 /** 064 * The type of the create schema task to execute. 065 * 066 * @parameter expression="hibernate" 067 * @required 068 */ 069 private String taskType; 070 071 /** 072 * @parameter expression="${project}" 073 * @required 074 * @readonly 075 */ 076 private MavenProject project; 077 078 /** 079 * Any property files that should be loaded into the schema properties. 080 * 081 * @parameter 082 */ 083 private String[] propertyFiles; 084 085 /** 086 * The properties that can be passed to the schema task. 087 * 088 * @parameter 089 */ 090 private Properties properties = new Properties(); 091 092 /** 093 * @component role="org.apache.maven.artifact.factory.ArtifactFactory" 094 * @required 095 * @readonly 096 */ 097 private ArtifactFactory factory; 098 099 /** 100 * Whether or not scripts should be executed (if this is set to false, they will 101 * only be generated, but not executed). 102 * 103 * @parameter expression="${executeScripts}" 104 */ 105 private boolean executeScripts = true; 106 107 /** 108 * @parameter expression="${plugin.artifacts}" 109 * @required 110 */ 111 private List<Artifact> pluginArtifacts; 112 113 /** 114 * Artifact resolver, needed to download source jars for inclusion in 115 * classpath. 116 * 117 * @component role="org.apache.maven.artifact.resolver.ArtifactResolver" 118 * @required 119 * @readonly 120 */ 121 private ArtifactResolver artifactResolver; 122 123 /** 124 * @parameter expression="${localRepository}" 125 * @required 126 * @readonly 127 */ 128 private ArtifactRepository localRepository; 129 130 /** 131 * The name of the JDBC driver class. 132 * 133 * @parameter 134 * @required 135 */ 136 private String jdbcDriver; 137 138 /** 139 * The JDBC connection URL. 140 * 141 * @parameter 142 * @required 143 */ 144 private String jdbcConnectionUrl; 145 146 /** 147 * The JDBC username for the database. 148 * 149 * @parameter 150 * @required 151 */ 152 private String jdbcUsername; 153 154 /** 155 * The JDBC password for the database. 156 * 157 * @parameter 158 */ 159 private String jdbcPassword = ""; 160 161 /** 162 * The jar containing the JDBC driver. 163 * 164 * @parameter 165 * @required 166 */ 167 private String jdbcDriverJar; 168 169 /** 170 * Defines the location(s) of any SQL scripts to be executed. 171 * 172 * @parameter 173 */ 174 private List<String> scripts; 175 176 /** 177 * @see org.apache.maven.plugin.Mojo#execute() 178 */ 179 public void execute() 180 throws MojoExecutionException, MojoFailureException 181 { 182 Connection connection = null; 183 try 184 { 185 AndroMDALogger.initialize(); 186 this.initializeClassLoaderWithJdbcDriver(); 187 188 final List<String> tasks = this.getTasks(); 189 if (tasks != null && !tasks.isEmpty()) 190 { 191 final Map<String, Class> tasksMap = SchemaMojo.tasksCache.get(this.taskType); 192 if (tasksMap == null) 193 { 194 throw new MojoExecutionException('\'' + taskType + 195 "' is not a valid task type, valid task types are: " + tasksMap.keySet()); 196 } 197 198 this.properties.putAll(this.project.getProperties()); 199 for (String task : this.getTasks()) 200 { 201 task = ObjectUtils.toString(task.trim()); 202 if (this.propertyFiles != null) 203 { 204 final int numberOfPropertyFiles = propertyFiles.length; 205 for (int ctr2 = 0; ctr2 < numberOfPropertyFiles; ctr2++) 206 { 207 final URL propertyFileUri = ResourceUtils.toURL(propertyFiles[ctr2]); 208 if (propertyFileUri != null) 209 { 210 final InputStream stream = propertyFileUri.openStream(); 211 this.properties.load(stream); 212 stream.close(); 213 } 214 } 215 } 216 217 // - load all the fields of this class into the properties 218 final Field[] fields = this.getClass().getDeclaredFields(); 219 if (fields != null) 220 { 221 final int numberOfFields = fields.length; 222 for (int ctr = 0; ctr < numberOfFields; ctr++) 223 { 224 final Field field = fields[ctr]; 225 final Object value = field.get(this); 226 if (value != null) 227 { 228 this.properties.put( 229 field.getName(), 230 value); 231 } 232 } 233 } 234 235 final Set<String> classpathElements = new LinkedHashSet<String>(this.project.getRuntimeClasspathElements()); 236 classpathElements.addAll(this.getProvidedClasspathElements()); 237 this.initializeClasspathFromClassPathElements(classpathElements); 238 final Class type = tasksMap.get(task); 239 if (type == null) 240 { 241 throw new MojoExecutionException('\'' + task + "' is not a valid task, valid types are: " + 242 tasksMap.keySet()); 243 } 244 245 final SchemaManagement schemaManagement = (SchemaManagement)ClassUtils.newInstance(type); 246 connection = executeScripts ? this.getConnection() : null; 247 this.executeSql( 248 connection, 249 schemaManagement.execute( 250 connection, 251 this.properties)); 252 } 253 } 254 255 // - execute any additional scripts 256 this.executeScripts(connection); 257 } 258 catch (final Throwable throwable) 259 { 260 throw new MojoExecutionException("An error occurred while attempting to create the schema", throwable); 261 } 262 finally 263 { 264 if (connection != null) 265 { 266 try 267 { 268 connection.close(); 269 } 270 catch (SQLException e) 271 { 272 // - ignore 273 } 274 } 275 } 276 } 277 278 /** 279 * Retrieves the tasks as a List. 280 * 281 * @return the tasks as a List. 282 */ 283 private List<String> getTasks() 284 { 285 return this.tasks != null ? Arrays.asList(this.tasks.split(",")) : null; 286 } 287 288 /** 289 * Executes any scripts found within the {@link #scriptLocations} and 290 * included using the {@link #scriptIncludes} 291 * 292 * @param connection the SQL connection used to execute the scripts. 293 * @throws MojoExecutionException 294 * @throws Exception 295 */ 296 private void executeScripts(final Connection connection) 297 throws MojoExecutionException 298 { 299 final List<String> tasks = this.getTasks(); 300 if (this.scripts != null && !this.scripts.isEmpty()) 301 { 302 for (final String location : scripts) 303 { 304 try 305 { 306 this.executeSql( 307 connection, 308 location); 309 } 310 catch (final Exception exception) 311 { 312 throw new MojoExecutionException("Execution failed on script: " + location, exception); 313 } 314 } 315 } 316 else if (tasks == null || tasks.isEmpty()) 317 { 318 this.getLog().info("No scripts found to execute"); 319 } 320 } 321 322 /** 323 * Sets the current context class loader from the given runtime classpath 324 * elements. 325 * @param classpathFiles 326 * @throws MalformedURLException 327 */ 328 protected void initializeClasspathFromClassPathElements(final Set<String> classpathFiles) 329 throws MalformedURLException 330 { 331 // - for some reason some of the plugin dependencies are being excluded from the classloader, 332 // so we explicitly load them 333 if (this.pluginArtifacts != null) 334 { 335 for (final Artifact artifact : this.pluginArtifacts) 336 { 337 final File artifactFile = artifact.getFile(); 338 if (artifactFile != null) 339 { 340 classpathFiles.add(artifactFile.toString()); 341 } 342 } 343 } 344 345 final List<String> files = new ArrayList<String>(classpathFiles); 346 if (!files.isEmpty()) 347 { 348 final URL[] classpathUrls = new URL[classpathFiles.size()]; 349 350 for (int ctr = 0; ctr < classpathFiles.size(); ++ctr) 351 { 352 final File file = new File(files.get(ctr)); 353 if (this.getLog().isDebugEnabled()) 354 { 355 getLog().debug("adding to classpath '" + file + '\''); 356 } 357 classpathUrls[ctr] = file.toURI().toURL(); 358 } 359 360 final URLClassLoader loader = 361 new URLClassLoader(classpathUrls, 362 Thread.currentThread().getContextClassLoader()); 363 Thread.currentThread().setContextClassLoader(loader); 364 } 365 } 366 367 /** 368 * Initializes the context class loader with the given 369 * <code>jdbcDriverJar</code> 370 * 371 * @throws MalformedURLException 372 */ 373 protected void initializeClassLoaderWithJdbcDriver() 374 throws MalformedURLException 375 { 376 Thread.currentThread().setContextClassLoader( 377 new URLClassLoader( 378 new URL[] {new File(this.jdbcDriverJar).toURI().toURL()}, 379 Thread.currentThread().getContextClassLoader())); 380 } 381 382 /** 383 * Adds any dependencies with a scope of 'provided' to the current project 384 * with a scope of runtime. 385 * @return classpathElements 386 * @throws ArtifactNotFoundException 387 * @throws ArtifactResolutionException 388 */ 389 protected List<String> getProvidedClasspathElements() 390 throws ArtifactResolutionException, ArtifactNotFoundException 391 { 392 final List<String> classpathElements = new ArrayList<String>(); 393 final List<Dependency> dependencies = this.project.getDependencies(); 394 if (dependencies != null && !dependencies.isEmpty()) 395 { 396 for (final Dependency dependency : dependencies) 397 { 398 if (Artifact.SCOPE_PROVIDED.equals(dependency.getScope())) 399 { 400 final String file = this.getDependencyFile(dependency); 401 if (file != null) 402 { 403 classpathElements.add(file); 404 } 405 } 406 } 407 } 408 return classpathElements; 409 } 410 411 /** 412 * Adds a dependency to the current project's dependencies. 413 * 414 * @param dependency 415 * @throws ArtifactNotFoundException 416 * @throws ArtifactResolutionException 417 */ 418 private String getDependencyFile(final Dependency dependency) 419 throws ArtifactResolutionException, ArtifactNotFoundException 420 { 421 String file = null; 422 if (dependency != null) 423 { 424 final Artifact artifact = 425 this.factory.createArtifact( 426 dependency.getGroupId(), 427 dependency.getArtifactId(), 428 dependency.getVersion(), 429 null, 430 dependency.getType()); 431 432 this.artifactResolver.resolve( 433 artifact, 434 project.getRemoteArtifactRepositories(), 435 this.localRepository); 436 file = artifact.getFile() != null ? artifact.getFile().toString() : null; 437 } 438 return file; 439 } 440 441 /** 442 * Retrieves a database connection, given the appropriate database 443 * information. 444 * 445 * @return the retrieved connection. 446 * @throws Exception 447 */ 448 protected Connection getConnection() 449 throws Exception 450 { 451 Driver driver = (Driver)ClassUtils.loadClass(this.jdbcDriver).newInstance(); 452 DriverManager.registerDriver(new JdbcDriverWrapper(driver)); 453 return DriverManager.getConnection( 454 this.jdbcConnectionUrl, 455 this.jdbcUsername, 456 this.jdbcPassword); 457 } 458 459 /** 460 * The statement end character. 461 */ 462 private static final String STATEMENT_END = ";"; 463 464 /** 465 * Executes the SQL contained with the file located at the 466 * <code>sqlPath</code>. 467 * 468 * @param connection the connection used to execute the SQL. 469 * @param sqlPath the path to the SQL file. 470 * @throws Exception 471 */ 472 public void executeSql( 473 final Connection connection, 474 final String sqlPath) 475 throws Exception 476 { 477 if (StringUtils.isNotBlank(sqlPath)) 478 { 479 final URL sqlUrl = ResourceUtils.toURL(sqlPath); 480 if (sqlUrl != null) 481 { 482 this.successes = 0; 483 this.failures = 0; 484 Statement statement = null; 485 if (connection != null) 486 { 487 statement = connection.createStatement(); 488 } 489 final InputStream stream = sqlUrl.openStream(); 490 final BufferedReader resourceInput = new BufferedReader(new InputStreamReader(stream)); 491 StringBuilder sql = new StringBuilder(); 492 for (String line = resourceInput.readLine(); line != null; line = resourceInput.readLine()) 493 { 494 if (line.startsWith("//")) 495 { 496 continue; 497 } 498 if (line.startsWith("--")) 499 { 500 continue; 501 } 502 sql.append(line); 503 if (line.endsWith(STATEMENT_END)) 504 { 505 if (statement != null) 506 { 507 this.executeSql( 508 statement, 509 sql.toString().replaceAll( 510 STATEMENT_END, 511 "")); 512 } 513 sql = new StringBuilder(); 514 } 515 sql.append('\n'); 516 } 517 resourceInput.close(); 518 if (statement != null) 519 { 520 statement.close(); 521 } 522 } 523 this.getLog().info(" Executed script: " + sqlPath); 524 final String count = String.valueOf(this.successes + this.failures); 525 this.getLog().info(' ' + count + " SQL statements executed"); 526 this.getLog().info(" Failures: " + this.failures); 527 this.getLog().info(" Successes: " + this.successes); 528 } 529 } 530 531 /** 532 * Stores the count of statements that were executed successfully. 533 */ 534 private int successes; 535 536 /** 537 * Stores the count of statements that failed. 538 */ 539 private int failures; 540 541 /** 542 * Executes the given <code>sql</code>, using the given 543 * <code>statement</code>. 544 * 545 * @param statement the statement to use to execute the SQL. 546 * @param sql the SQL to execute. 547 * @throws SQLException 548 */ 549 private void executeSql( 550 final Statement statement, 551 final String sql) 552 { 553 this.getLog().info(sql.trim()); 554 try 555 { 556 statement.execute(sql); 557 this.successes++; 558 } 559 catch (final SQLException exception) 560 { 561 this.failures++; 562 this.getLog().warn(exception.toString()); 563 } 564 } 565 566 /** 567 * Stores the task types. 568 */ 569 private static final HashMap<String, Map<String, Class>> tasksCache = new LinkedHashMap<String, Map<String, Class>>(); 570 571 static 572 { 573 // - initialize the hibernate task types 574 final Map<String, Class> hibernateTasks = new LinkedHashMap<String, Class>(); 575 tasksCache.put( 576 "hibernate", 577 hibernateTasks); 578 hibernateTasks.put( 579 "create", 580 HibernateCreateSchema.class); 581 hibernateTasks.put( 582 "drop", 583 HibernateDropSchema.class); 584 hibernateTasks.put( 585 "update", 586 HibernateUpdateSchema.class); 587 hibernateTasks.put( 588 "validate", 589 HibernateValidateSchema.class); 590 } 591}