f016459f7d60cdacca4b739bf1c1d839870118df
[hibernate4-maven-plugin] / src / main / java / de / juplo / plugins / hibernate4 / Hbm2DdlMojo.java
1 package de.juplo.plugins.hibernate4;
2
3 /*
4  * Copyright 2001-2005 The Apache Software Foundation.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *      http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18
19 import com.pyx4j.log4j.MavenLogAppender;
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.IOException;
23 import java.net.URL;
24 import java.net.URLClassLoader;
25 import java.sql.Connection;
26 import java.sql.Driver;
27 import java.sql.DriverManager;
28 import java.sql.DriverPropertyInfo;
29 import java.sql.SQLException;
30 import java.sql.SQLFeatureNotSupportedException;
31 import java.util.Enumeration;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Map.Entry;
35 import java.util.Properties;
36 import java.util.Set;
37 import java.util.logging.Logger;
38 import javax.persistence.Embeddable;
39 import javax.persistence.Entity;
40 import javax.persistence.MappedSuperclass;
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 import org.hibernate.cfg.Configuration;
46 import org.hibernate.tool.hbm2ddl.SchemaExport;
47 import org.hibernate.tool.hbm2ddl.SchemaExport.Type;
48 import org.hibernate.tool.hbm2ddl.Target;
49 import org.scannotation.AnnotationDB;
50
51
52 /**
53  * Goal which extracts the hibernate-mapping-configuration and
54  * exports an according SQL-database-schema.
55  *
56  * @goal export
57  * @phase process-classes
58  * @threadSafe
59  * @requiresDependencyResolution runtime
60  */
61 public class Hbm2DdlMojo extends AbstractMojo
62 {
63   public final static String DRIVER_CLASS = "hibernate.connection.driver_class";
64   public final static String URL = "hibernate.connection.url";
65   public final static String USERNAME = "hibernate.connection.username";
66   public final static String PASSWORD = "hibernate.connection.password";
67   public final static String DIALECT = "hibernate.dialect";
68
69
70   /**
71    * The project whose project files to create.
72    *
73    * @parameter expression="${project}"
74    * @required
75    * @readonly
76    */
77   private MavenProject project;
78
79   /**
80    * Directories to scan.
81    *
82    * @parameter expression="${project.build.outputDirectory}"
83    */
84   private String outputDirectory;
85
86   /**
87    * Skip execution
88    *
89    * @parameter expression="${maven.test.skip}"
90    */
91   private boolean skip;
92
93   /**
94    * SQL-Driver name.
95    *
96    * @parameter expression="${hibernate.connection.driver_class}
97    */
98   private String driverClassName;
99
100   /**
101    * Database URL.
102    *
103    * @parameter expression="${hibernate.connection.url}"
104    */
105   private String url;
106
107   /**
108    * Database username
109    *
110    * @parameter expression="${hibernate.connection.username}"
111    */
112   private String username;
113
114   /**
115    * Database password
116    *
117    * @parameter expression="${hibernate.connection.password}"
118    */
119   private String password;
120
121   /**
122    * Hibernate dialect.
123    *
124    * @parameter expression="${hibernate.dialect}"
125    */
126   private String hibernateDialect;
127
128   /**
129    * Hibernate configuration file.
130    *
131    * @parameter default-value="${project.build.outputDirectory}/hibernate.properties"
132    */
133   private String hibernateProperties;
134
135   /**
136    * Target of execution:
137    * <ul>
138    *   <li><strong>NONE</strong> do nothing - just validate the configuration</li>
139    *   <li><strong>EXPORT</strong> create database <strong>(DEFAULT!)</strong></li>
140    *   <li><strong>SCRIPT</strong> export schema to SQL-script</li>
141    *   <li><strong>BOTH</strong></li>
142    * </ul>
143    * @parameter default-value="EXPORT"
144    */
145   private String target;
146
147   /**
148    * Type of export.
149    * <ul>
150    *   <li><strong>NONE</strong> do nothing - just validate the configuration</li>
151    *   <li><strong>CREATE</strong> create database-schema</li>
152    *   <li><strong>DROP</strong> drop database-schema</li>
153    *   <li><strong>BOTH</strong> <strong>(DEFAULT!)</strong></li>
154    * </ul>
155    * @parameter default-value="BOTH"
156    */
157   private String type;
158
159   /**
160    * Output file.
161    *
162    * @parameter default-value="${project.build.outputDirectory}/schema.sql"
163    */
164   private String outputFile;
165
166   /**
167    * Delimiter in output-file.
168    *
169    * @parameter default-value=";"
170    */
171   private String delimiter;
172
173   /**
174    * Format output-file.
175    *
176    * @parameter default-value="true"
177    */
178   private boolean format;
179
180
181   @Override
182   public void execute()
183     throws
184       MojoFailureException,
185       MojoExecutionException
186   {
187     if (skip)
188     {
189       getLog().info("Exectuion of hibernate4-maven-plugin:export was skipped!");
190       return;
191     }
192
193     File dir = new File(outputDirectory);
194     if (!dir.exists())
195       throw new MojoExecutionException("Cannot scan for annotated classes in " + outputDirectory + ": directory does not exist!");
196
197
198     Set<String> classes = new HashSet<String>();
199     try
200     {
201       AnnotationDB db = new AnnotationDB();
202       getLog().info("Scanning directory " + outputDirectory + " for annotated classes...");
203       URL dirUrl = dir.toURI().toURL();
204       db.scanArchives(dirUrl);
205       if (db.getAnnotationIndex().containsKey(Entity.class.getName()))
206         classes.addAll(db.getAnnotationIndex().get(Entity.class.getName()));
207       if (db.getAnnotationIndex().containsKey(MappedSuperclass.class.getName()))
208         classes.addAll(db.getAnnotationIndex().get(MappedSuperclass.class.getName()));
209       if (db.getAnnotationIndex().containsKey(Embeddable.class.getName()))
210         classes.addAll(db.getAnnotationIndex().get(Embeddable.class.getName()));
211     }
212     catch (IOException e)
213     {
214       getLog().error("Error while scanning!", e);
215       throw new MojoFailureException(e.getMessage());
216     }
217     if (classes.isEmpty())
218       throw new MojoFailureException("No annotated classes found in directory " + outputDirectory);
219
220     Properties properties = new Properties();
221
222     /** Try to read configuration from properties-file */
223     try
224     {
225       File file = new File(hibernateProperties);
226       if (file.exists())
227       {
228         getLog().info("Reading properties from file " + hibernateProperties + "...");
229         properties.load(new FileInputStream(file));
230       }
231       else
232         getLog().info("No hibernate-properties-file found! Checked path: " + hibernateProperties);
233     }
234     catch (IOException e)
235     {
236       getLog().error("Error while reading properties!", e);
237       throw new MojoExecutionException(e.getMessage());
238     }
239
240     /** Overwrite values from propertie-file or set, if given */
241     if (driverClassName != null)
242     {
243       if (properties.containsKey(DRIVER_CLASS))
244         getLog().debug(
245             "Overwriting property " +
246             DRIVER_CLASS + "=" + properties.getProperty(DRIVER_CLASS) +
247             " with the value " + driverClassName +
248             " from the plugin-configuration-parameter driverClassName!"
249           );
250       else
251         getLog().debug(
252             "Using the value " + driverClassName +
253             " from the plugin-configuration-parameter driverClassName!"
254           );
255       properties.setProperty(DRIVER_CLASS, driverClassName);
256     }
257     if (url != null)
258     {
259       if (properties.containsKey(URL))
260         getLog().debug(
261             "Overwriting property " +
262             URL + "=" + properties.getProperty(URL) +
263             " with the value " + url +
264             " from the plugin-configuration-parameter url!"
265           );
266       else
267         getLog().debug(
268             "Using the value " + url +
269             " from the plugin-configuration-parameter url!"
270           );
271       properties.setProperty(URL, url);
272     }
273     if (username != null)
274     {
275       if (properties.containsKey(USERNAME))
276         getLog().debug(
277             "Overwriting property " +
278             USERNAME + "=" + properties.getProperty(USERNAME) +
279             " with the value " + username +
280             " from the plugin-configuration-parameter username!"
281           );
282       else
283         getLog().debug(
284             "Using the value " + username +
285             " from the plugin-configuration-parameter username!"
286           );
287       properties.setProperty(USERNAME, username);
288     }
289     if (password != null)
290     {
291       if (properties.containsKey(PASSWORD))
292         getLog().debug(
293             "Overwriting property " +
294             PASSWORD + "=" + properties.getProperty(PASSWORD) +
295             " with the value " + password +
296             " from the plugin-configuration-parameter password!"
297           );
298       else
299         getLog().debug(
300             "Using the value " + password +
301             " from the plugin-configuration-parameter password!"
302           );
303       properties.setProperty(PASSWORD, password);
304     }
305     if (hibernateDialect != null)
306     {
307       if (properties.containsKey(DIALECT))
308         getLog().debug(
309             "Overwriting property " +
310             DIALECT + "=" + properties.getProperty(DIALECT) +
311             " with the value " + hibernateDialect +
312             " from the plugin-configuration-parameter hibernateDialect!"
313           );
314       else
315         getLog().debug(
316             "Using the value " + hibernateDialect +
317             " from the plugin-configuration-parameter hibernateDialect!"
318           );
319       properties.setProperty(DIALECT, hibernateDialect);
320     }
321
322     getLog().info("Gathered hibernate-configuration (turn on debugging for details):");
323     if (properties.isEmpty())
324     {
325       getLog().error("No properties set!");
326       throw new MojoFailureException("Hibernate-Configuration is missing!");
327     }
328     for (Entry<Object,Object> entry : properties.entrySet())
329       getLog().info("  " + entry.getKey() + " = " + entry.getValue());
330
331     ClassLoader classLoader = null;
332     try
333     {
334       getLog().debug("Creating ClassLoader for project-dependencies...");
335       List<String> classpathFiles = project.getCompileClasspathElements();
336       URL[] urls = new URL[classpathFiles.size()];
337       for (int i = 0; i < classpathFiles.size(); ++i)
338       {
339         getLog().debug("Dependency: " + classpathFiles.get(i));
340         urls[i] = new File(classpathFiles.get(i)).toURI().toURL();
341       }
342       classLoader = new URLClassLoader(urls, getClass().getClassLoader());
343     }
344     catch (Exception e)
345     {
346       getLog().error("Error while creating ClassLoader!", e);
347       throw new MojoExecutionException(e.getMessage());
348     }
349
350     Configuration config = new Configuration();
351     config.setProperties(properties);
352     try
353     {
354       getLog().debug("Adding annotated classes to hibernate-mapping-configuration...");
355       for (String annotatedClass : classes)
356       {
357         getLog().debug("Class " + annotatedClass);
358         config.addAnnotatedClass(classLoader.loadClass(annotatedClass));
359       }
360     }
361     catch (ClassNotFoundException e)
362     {
363       getLog().error("Error while adding annotated classes!", e);
364       throw new MojoExecutionException(e.getMessage());
365     }
366
367     Target target = null;
368     try
369     {
370       target = Target.valueOf(this.target);
371     }
372     catch (IllegalArgumentException e)
373     {
374       getLog().error("Invalid value for configuration-option \"target\": " + this.target);
375       getLog().error("Valid values are: NONE, SCRIPT, EXPORT, BOTH");
376       throw new MojoExecutionException("Invalid value for configuration-option \"target\"");
377     }
378     Type type = null;
379     try
380     {
381       type = Type.valueOf(this.type);
382     }
383     catch (IllegalArgumentException e)
384     {
385       getLog().error("Invalid value for configuration-option \"type\": " + this.type);
386       getLog().error("Valid values are: NONE, CREATE, DROP, BOTH");
387       throw new MojoExecutionException("Invalid value for configuration-option \"type\"");
388     }
389
390     Connection connection = null;
391     try
392     {
393       /**
394        * The connection must be established outside of hibernate, because
395        * hibernate does not use the context-classloader of the current
396        * thread and, hence, would not be able to resolve the driver-class!
397        */
398       switch (target)
399       {
400         case EXPORT:
401         case BOTH:
402           switch (type)
403           {
404             case CREATE:
405             case DROP:
406             case BOTH:
407               Class driverClass = classLoader.loadClass(driverClassName);
408               getLog().debug("Registering JDBC-driver " + driverClass.getName());
409               DriverManager.registerDriver(new DriverProxy((Driver)driverClass.newInstance()));
410               getLog().debug("Opening JDBC-connection to " + url + " as " + username + " with password " + password);
411               connection = DriverManager.getConnection(url, username, password);
412           }
413       }
414     }
415     catch (ClassNotFoundException e)
416     {
417       getLog().error("Dependency for driver-class " + driverClassName + " is missing!");
418       throw new MojoExecutionException(e.getMessage());
419     }
420     catch (Exception e)
421     {
422       getLog().error("Cannot establish connection to database!");
423       Enumeration<Driver> drivers = DriverManager.getDrivers();
424       if (!drivers.hasMoreElements())
425         getLog().error("No drivers registered!");
426       while (drivers.hasMoreElements())
427         getLog().debug("Driver: " + drivers.nextElement());
428       throw new MojoExecutionException(e.getMessage());
429     }
430
431     ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
432     MavenLogAppender.startPluginLog(this);
433     try
434     {
435       /**
436        * Change class-loader of current thread, so that hibernate can
437        * see all dependencies!
438        */
439       Thread.currentThread().setContextClassLoader(classLoader);
440
441       SchemaExport export = new SchemaExport(config, connection);
442       export.setOutputFile(outputFile);
443       export.setDelimiter(delimiter);
444       export.setFormat(format);
445       export.execute(target, type);
446
447       for (Object exception : export.getExceptions())
448         getLog().debug(exception.toString());
449     }
450     finally
451     {
452       /** Stop Log-Capturing */
453       MavenLogAppender.endPluginLog(this);
454
455       /** Restore the old class-loader (TODO: is this really necessary?) */
456       Thread.currentThread().setContextClassLoader(contextClassLoader);
457
458       /** Close the connection */
459       try
460       {
461         connection.close();
462       }
463       catch (SQLException e)
464       {
465         getLog().error("Error while closing connection: " + e.getMessage());
466       }
467     }
468   }
469
470   /**
471    * Needed, because DriverManager won't pick up drivers, that were not
472    * loaded by the system-classloader!
473    * See:
474    * http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-from-an-arbitrary-location
475    */
476   static final class DriverProxy implements Driver
477   {
478     private final Driver target;
479
480     DriverProxy(Driver target)
481     {
482       if (target == null)
483         throw new NullPointerException();
484       this.target = target;
485     }
486
487     public java.sql.Driver getTarget()
488     {
489       return target;
490     }
491
492     @Override
493     public boolean acceptsURL(String url) throws SQLException
494     {
495       return target.acceptsURL(url);
496     }
497
498     @Override
499     public java.sql.Connection connect(
500         String url,
501         java.util.Properties info
502       )
503       throws
504         SQLException
505     {
506       return target.connect(url, info);
507     }
508
509     @Override
510     public int getMajorVersion()
511     {
512       return target.getMajorVersion();
513     }
514
515     @Override
516     public int getMinorVersion()
517     {
518       return target.getMinorVersion();
519     }
520
521     @Override
522     public DriverPropertyInfo[] getPropertyInfo(
523         String url,
524         Properties info
525       )
526       throws
527         SQLException
528     {
529       return target.getPropertyInfo(url, info);
530     }
531
532     @Override
533     public boolean jdbcCompliant()
534     {
535       return target.jdbcCompliant();
536     }
537
538     /**
539      * This Method cannot be annotated with @Override, becaus the plugin
540      * will not compile then under Java 1.6!
541      */
542     public Logger getParentLogger() throws SQLFeatureNotSupportedException
543     {
544       throw new SQLFeatureNotSupportedException("Not supported, for backward-compatibility with Java 1.6");
545     }
546
547     @Override
548     public String toString()
549     {
550       return "Proxy: " + target;
551     }
552
553     @Override
554     public int hashCode()
555     {
556       return target.hashCode();
557     }
558
559     @Override
560     public boolean equals(Object obj)
561     {
562       if (!(obj instanceof DriverProxy))
563         return false;
564       DriverProxy other = (DriverProxy) obj;
565       return this.target.equals(other.target);
566     }
567   }
568 }