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