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