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