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}