7c5cc340d10a1d9304f928b697d3b30f82844257
[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       return;
189
190     File dir = new File(outputDirectory);
191     if (!dir.exists())
192       throw new MojoExecutionException("Cannot scan for annotated classes in " + outputDirectory + ": directory does not exist!");
193
194
195     Set<String> classes = new HashSet<String>();
196     try
197     {
198       AnnotationDB db = new AnnotationDB();
199       getLog().info("Scanning directory " + outputDirectory + " for annotated classes...");
200       URL dirUrl = dir.toURI().toURL();
201       db.scanArchives(dirUrl);
202       if (db.getAnnotationIndex().containsKey(Entity.class.getName()))
203         classes.addAll(db.getAnnotationIndex().get(Entity.class.getName()));
204       if (db.getAnnotationIndex().containsKey(MappedSuperclass.class.getName()))
205         classes.addAll(db.getAnnotationIndex().get(MappedSuperclass.class.getName()));
206       if (db.getAnnotationIndex().containsKey(Embeddable.class.getName()))
207         classes.addAll(db.getAnnotationIndex().get(Embeddable.class.getName()));
208     }
209     catch (IOException e)
210     {
211       getLog().error("Error while scanning!", e);
212       throw new MojoFailureException(e.getMessage());
213     }
214     if (classes.isEmpty())
215       throw new MojoFailureException("No annotated classes found in directory " + outputDirectory);
216
217     Properties properties = new Properties();
218
219     /** Try to read configuration from properties-file */
220     try
221     {
222       File file = new File(hibernateProperties);
223       if (file.exists())
224       {
225         getLog().info("Reading properties from file " + hibernateProperties + "...");
226         properties.load(new FileInputStream(file));
227       }
228       else
229         getLog().info("No hibernate-properties-file found! Checked path: " + hibernateProperties);
230     }
231     catch (IOException e)
232     {
233       getLog().error("Error while reading properties!", e);
234       throw new MojoExecutionException(e.getMessage());
235     }
236
237     /** Overwrite values from propertie-file or set, if given */
238     if (driverClassName != null)
239     {
240       if (properties.containsKey(DRIVER_CLASS))
241         getLog().debug(
242             "Overwriting property " +
243             DRIVER_CLASS + "=" + properties.getProperty(DRIVER_CLASS) +
244             " with the value " + driverClassName +
245             " from the plugin-configuration-parameter driverClassName!"
246           );
247       else
248         getLog().debug(
249             "Using the value " + driverClassName +
250             " from the plugin-configuration-parameter driverClassName!"
251           );
252       properties.setProperty(DRIVER_CLASS, driverClassName);
253     }
254     if (url != null)
255     {
256       if (properties.containsKey(URL))
257         getLog().debug(
258             "Overwriting property " +
259             URL + "=" + properties.getProperty(URL) +
260             " with the value " + url +
261             " from the plugin-configuration-parameter url!"
262           );
263       else
264         getLog().debug(
265             "Using the value " + url +
266             " from the plugin-configuration-parameter url!"
267           );
268       properties.setProperty(URL, url);
269     }
270     if (username != null)
271     {
272       if (properties.containsKey(USERNAME))
273         getLog().debug(
274             "Overwriting property " +
275             USERNAME + "=" + properties.getProperty(USERNAME) +
276             " with the value " + username +
277             " from the plugin-configuration-parameter username!"
278           );
279       else
280         getLog().debug(
281             "Using the value " + username +
282             " from the plugin-configuration-parameter username!"
283           );
284       properties.setProperty(USERNAME, username);
285     }
286     if (password != null)
287     {
288       if (properties.containsKey(PASSWORD))
289         getLog().debug(
290             "Overwriting property " +
291             PASSWORD + "=" + properties.getProperty(PASSWORD) +
292             " with the value " + password +
293             " from the plugin-configuration-parameter password!"
294           );
295       else
296         getLog().debug(
297             "Using the value " + password +
298             " from the plugin-configuration-parameter password!"
299           );
300       properties.setProperty(PASSWORD, password);
301     }
302     if (hibernateDialect != null)
303     {
304       if (properties.containsKey(DIALECT))
305         getLog().debug(
306             "Overwriting property " +
307             DIALECT + "=" + properties.getProperty(DIALECT) +
308             " with the value " + hibernateDialect +
309             " from the plugin-configuration-parameter hibernateDialect!"
310           );
311       else
312         getLog().debug(
313             "Using the value " + hibernateDialect +
314             " from the plugin-configuration-parameter hibernateDialect!"
315           );
316       properties.setProperty(DIALECT, hibernateDialect);
317     }
318
319     getLog().info("Gathered hibernate-configuration (turn on debugging for details):");
320     if (properties.isEmpty())
321     {
322       getLog().error("No properties set!");
323       throw new MojoFailureException("Hibernate-Configuration is missing!");
324     }
325     for (Entry<Object,Object> entry : properties.entrySet())
326       getLog().info("  " + entry.getKey() + " = " + entry.getValue());
327
328     ClassLoader classLoader = null;
329     try
330     {
331       getLog().debug("Creating ClassLoader for project-dependencies...");
332       List<String> classpathFiles = project.getCompileClasspathElements();
333       URL[] urls = new URL[classpathFiles.size()];
334       for (int i = 0; i < classpathFiles.size(); ++i)
335       {
336         getLog().debug("Dependency: " + classpathFiles.get(i));
337         urls[i] = new File(classpathFiles.get(i)).toURI().toURL();
338       }
339       classLoader = new URLClassLoader(urls, getClass().getClassLoader());
340     }
341     catch (Exception e)
342     {
343       getLog().error("Error while creating ClassLoader!", e);
344       throw new MojoExecutionException(e.getMessage());
345     }
346
347     Configuration config = new Configuration();
348     config.setProperties(properties);
349     try
350     {
351       getLog().debug("Adding annotated classes to hibernate-mapping-configuration...");
352       for (String annotatedClass : classes)
353       {
354         getLog().debug("Class " + annotatedClass);
355         config.addAnnotatedClass(classLoader.loadClass(annotatedClass));
356       }
357     }
358     catch (ClassNotFoundException e)
359     {
360       getLog().error("Error while adding annotated classes!", e);
361       throw new MojoExecutionException(e.getMessage());
362     }
363
364     Target target = null;
365     try
366     {
367       target = Target.valueOf(this.target);
368     }
369     catch (IllegalArgumentException e)
370     {
371       getLog().error("Invalid value for configuration-option \"target\": " + this.target);
372       getLog().error("Valid values are: NONE, SCRIPT, EXPORT, BOTH");
373       throw new MojoExecutionException("Invalid value for configuration-option \"target\"");
374     }
375     Type type = null;
376     try
377     {
378       type = Type.valueOf(this.type);
379     }
380     catch (IllegalArgumentException e)
381     {
382       getLog().error("Invalid value for configuration-option \"type\": " + this.type);
383       getLog().error("Valid values are: NONE, CREATE, DROP, BOTH");
384       throw new MojoExecutionException("Invalid value for configuration-option \"type\"");
385     }
386
387     Connection connection = null;
388     try
389     {
390       /**
391        * The connection must be established outside of hibernate, because
392        * hibernate does not use the context-classloader of the current
393        * thread and, hence, would not be able to resolve the driver-class!
394        */
395       switch (target)
396       {
397         case EXPORT:
398         case BOTH:
399           switch (type)
400           {
401             case CREATE:
402             case DROP:
403             case BOTH:
404               Class driverClass = classLoader.loadClass(driverClassName);
405               getLog().debug("Registering JDBC-driver " + driverClass.getName());
406               DriverManager.registerDriver(new DriverProxy((Driver)driverClass.newInstance()));
407               getLog().debug("Opening JDBC-connection to " + url + " as " + username + " with password " + password);
408               connection = DriverManager.getConnection(url, username, password);
409           }
410       }
411     }
412     catch (ClassNotFoundException e)
413     {
414       getLog().error("Dependency for driver-class " + driverClassName + " is missing!");
415       throw new MojoExecutionException(e.getMessage());
416     }
417     catch (Exception e)
418     {
419       getLog().error("Cannot establish connection to database!");
420       Enumeration<Driver> drivers = DriverManager.getDrivers();
421       if (!drivers.hasMoreElements())
422         getLog().error("No drivers registered!");
423       while (drivers.hasMoreElements())
424         getLog().debug("Driver: " + drivers.nextElement());
425       throw new MojoExecutionException(e.getMessage());
426     }
427
428     ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
429     MavenLogAppender.startPluginLog(this);
430     try
431     {
432       /**
433        * Change class-loader of current thread, so that hibernate can
434        * see all dependencies!
435        */
436       Thread.currentThread().setContextClassLoader(classLoader);
437
438       SchemaExport export = new SchemaExport(config, connection);
439       export.setOutputFile(outputFile);
440       export.setDelimiter(delimiter);
441       export.setFormat(format);
442       export.execute(target, type);
443
444       for (Object exception : export.getExceptions())
445         getLog().debug(exception.toString());
446     }
447     finally
448     {
449       /** Stop Log-Capturing */
450       MavenLogAppender.endPluginLog(this);
451
452       /** Restore the old class-loader (TODO: is this really necessary?) */
453       Thread.currentThread().setContextClassLoader(contextClassLoader);
454
455       /** Close the connection */
456       try
457       {
458         connection.close();
459       }
460       catch (SQLException e)
461       {
462         getLog().error("Error while closing connection: " + e.getMessage());
463       }
464     }
465   }
466
467   /**
468    * Needed, because DriverManager won't pick up drivers, that were not
469    * loaded by the system-classloader!
470    * See:
471    * http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-from-an-arbitrary-location
472    */
473   static final class DriverProxy implements Driver
474   {
475     private final Driver target;
476
477     DriverProxy(Driver target)
478     {
479       if (target == null)
480         throw new NullPointerException();
481       this.target = target;
482     }
483
484     public java.sql.Driver getTarget()
485     {
486       return target;
487     }
488
489     @Override
490     public boolean acceptsURL(String url) throws SQLException
491     {
492       return target.acceptsURL(url);
493     }
494
495     @Override
496     public java.sql.Connection connect(
497         String url,
498         java.util.Properties info
499       )
500       throws
501         SQLException
502     {
503       return target.connect(url, info);
504     }
505
506     @Override
507     public int getMajorVersion()
508     {
509       return target.getMajorVersion();
510     }
511
512     @Override
513     public int getMinorVersion()
514     {
515       return target.getMinorVersion();
516     }
517
518     @Override
519     public DriverPropertyInfo[] getPropertyInfo(
520         String url,
521         Properties info
522       )
523       throws
524         SQLException
525     {
526       return target.getPropertyInfo(url, info);
527     }
528
529     @Override
530     public boolean jdbcCompliant()
531     {
532       return target.jdbcCompliant();
533     }
534
535     /**
536      * This Method cannot be annotated with @Override, becaus the plugin
537      * will not compile then under Java 1.6!
538      */
539     public Logger getParentLogger() throws SQLFeatureNotSupportedException
540     {
541       throw new SQLFeatureNotSupportedException("Not supported, for backward-compatibility with Java 1.6");
542     }
543
544     @Override
545     public String toString()
546     {
547       return "Proxy: " + target;
548     }
549
550     @Override
551     public int hashCode()
552     {
553       return target.hashCode();
554     }
555
556     @Override
557     public boolean equals(Object obj)
558     {
559       if (!(obj instanceof DriverProxy))
560         return false;
561       DriverProxy other = (DriverProxy) obj;
562       return this.target.equals(other.target);
563     }
564   }
565 }