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