View Javadoc
1   package org.andromda.maven.plugin.andromdapp;
2   
3   import java.io.BufferedReader;
4   import java.io.File;
5   import java.io.InputStream;
6   import java.io.InputStreamReader;
7   import java.lang.reflect.Field;
8   import java.net.MalformedURLException;
9   import java.net.URL;
10  import java.net.URLClassLoader;
11  import java.sql.Connection;
12  import java.sql.Driver;
13  import java.sql.DriverManager;
14  import java.sql.SQLException;
15  import java.sql.Statement;
16  import java.util.ArrayList;
17  import java.util.Arrays;
18  import java.util.HashMap;
19  import java.util.LinkedHashMap;
20  import java.util.LinkedHashSet;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.Properties;
24  import java.util.Set;
25  import org.andromda.core.common.AndroMDALogger;
26  import org.andromda.core.common.ClassUtils;
27  import org.andromda.core.common.ResourceUtils;
28  import org.andromda.maven.plugin.andromdapp.hibernate.HibernateCreateSchema;
29  import org.andromda.maven.plugin.andromdapp.hibernate.HibernateDropSchema;
30  import org.andromda.maven.plugin.andromdapp.hibernate.HibernateUpdateSchema;
31  import org.andromda.maven.plugin.andromdapp.hibernate.HibernateValidateSchema;
32  import org.apache.commons.lang.ObjectUtils;
33  import org.apache.commons.lang.StringUtils;
34  import org.apache.maven.artifact.Artifact;
35  import org.apache.maven.artifact.factory.ArtifactFactory;
36  import org.apache.maven.artifact.repository.ArtifactRepository;
37  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
38  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
39  import org.apache.maven.artifact.resolver.ArtifactResolver;
40  import org.apache.maven.model.Dependency;
41  import org.apache.maven.plugin.AbstractMojo;
42  import org.apache.maven.plugin.MojoExecutionException;
43  import org.apache.maven.plugin.MojoFailureException;
44  import org.apache.maven.project.MavenProject;
45  
46  /**
47   * Provides the ability to drop database schemas.
48   *
49   * @goal schema
50   * @requiresDependencyResolution runtime
51   * @author Chad Brandon
52   */
53  public class SchemaMojo
54      extends AbstractMojo
55  {
56      /**
57       * The schema task to execute (create, drop, update, validate)
58       *
59       * @parameter expression="${tasks}"
60       */
61      private String tasks;
62  
63      /**
64       * The type of the create schema task to execute.
65       *
66       * @parameter expression="hibernate"
67       * @required
68       */
69      private String taskType;
70  
71      /**
72       * @parameter expression="${project}"
73       * @required
74       * @readonly
75       */
76      private MavenProject project;
77  
78      /**
79       * Any property files that should be loaded into the schema properties.
80       *
81       * @parameter
82       */
83      private String[] propertyFiles;
84  
85      /**
86       * The properties that can be passed to the schema task.
87       *
88       * @parameter
89       */
90      private Properties properties = new Properties();
91  
92      /**
93       * @component role="org.apache.maven.artifact.factory.ArtifactFactory"
94       * @required
95       * @readonly
96       */
97      private ArtifactFactory factory;
98  
99      /**
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 }