WIP:site
[hibernate4-maven-plugin] / 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             " from the plugin-configuration-parameter driverClassName!"
382           );
383       else
384         getLog().debug(
385             "Using the value " + driverClassName +
386             " from the plugin-configuration-parameter driverClassName!"
387           );
388       properties.setProperty(DRIVER_CLASS, driverClassName);
389     }
390     if (url != null)
391     {
392       if (properties.containsKey(URL))
393         getLog().debug(
394             "Overwriting property " +
395             URL + "=" + properties.getProperty(URL) +
396             " with the value " + url +
397             " from the plugin-configuration-parameter url!"
398           );
399       else
400         getLog().debug(
401             "Using the value " + url +
402             " from the plugin-configuration-parameter url!"
403           );
404       properties.setProperty(URL, url);
405     }
406     if (username != null)
407     {
408       if (properties.containsKey(USERNAME))
409         getLog().debug(
410             "Overwriting property " +
411             USERNAME + "=" + properties.getProperty(USERNAME) +
412             " with the value " + username +
413             " from the plugin-configuration-parameter username!"
414           );
415       else
416         getLog().debug(
417             "Using the value " + username +
418             " from the plugin-configuration-parameter username!"
419           );
420       properties.setProperty(USERNAME, username);
421     }
422     if (password != null)
423     {
424       if (properties.containsKey(PASSWORD))
425         getLog().debug(
426             "Overwriting property " +
427             PASSWORD + "=" + properties.getProperty(PASSWORD) +
428             " with the value " + password +
429             " from the plugin-configuration-parameter password!"
430           );
431       else
432         getLog().debug(
433             "Using the value " + password +
434             " from the plugin-configuration-parameter password!"
435           );
436       properties.setProperty(PASSWORD, password);
437     }
438     if (hibernateDialect != null)
439     {
440       if (properties.containsKey(DIALECT))
441         getLog().debug(
442             "Overwriting property " +
443             DIALECT + "=" + properties.getProperty(DIALECT) +
444             " with the value " + hibernateDialect +
445             " from the plugin-configuration-parameter hibernateDialect!"
446           );
447       else
448         getLog().debug(
449             "Using the value " + hibernateDialect +
450             " from the plugin-configuration-parameter hibernateDialect!"
451           );
452       properties.setProperty(DIALECT, hibernateDialect);
453     }
454
455     /** The generated SQL varies with the dialect! */
456     if (md5s.containsKey(DIALECT))
457     {
458       String dialect = properties.getProperty(DIALECT);
459       if (md5s.get(DIALECT).equals(dialect))
460         getLog().debug("SQL-dialect unchanged.");
461       else
462       {
463         getLog().debug("SQL-dialect changed: " + dialect);
464         modified = true;
465         md5s.put(DIALECT, dialect);
466       }
467     }
468     else
469     {
470       modified = true;
471       md5s.put(DIALECT, properties.getProperty(DIALECT));
472     }
473
474     if (properties.isEmpty())
475     {
476       getLog().error("No properties set!");
477       throw new MojoFailureException("Hibernate-Configuration is missing!");
478     }
479
480     Configuration config = new Configuration();
481     config.setProperties(properties);
482     getLog().debug("Adding annotated classes to hibernate-mapping-configuration...");
483     for (Class<?> annotatedClass : classes)
484     {
485       getLog().debug("Class " + annotatedClass);
486       config.addAnnotatedClass(annotatedClass);
487     }
488
489     Target target = null;
490     try
491     {
492       target = Target.valueOf(this.target.toUpperCase());
493     }
494     catch (IllegalArgumentException e)
495     {
496       getLog().error("Invalid value for configuration-option \"target\": " + this.target);
497       getLog().error("Valid values are: NONE, SCRIPT, EXPORT, BOTH");
498       throw new MojoExecutionException("Invalid value for configuration-option \"target\"");
499     }
500     Type type = null;
501     try
502     {
503       type = Type.valueOf(this.type.toUpperCase());
504     }
505     catch (IllegalArgumentException e)
506     {
507       getLog().error("Invalid value for configuration-option \"type\": " + this.type);
508       getLog().error("Valid values are: NONE, CREATE, DROP, BOTH");
509       throw new MojoExecutionException("Invalid value for configuration-option \"type\"");
510     }
511
512     if (target.equals(Target.SCRIPT) || target.equals(Target.NONE))
513     {
514       project.getProperties().setProperty(EXPORT_SKIPPED_PROPERTY, "true");
515     }
516     if (
517         !modified
518         && !target.equals(Target.SCRIPT)
519         && !target.equals(Target.NONE)
520         && !force
521       )
522     {
523       getLog().info("No modified annotated classes found and dialect unchanged.");
524       getLog().info("Skipping schema generation!");
525       project.getProperties().setProperty(EXPORT_SKIPPED_PROPERTY, "true");
526       return;
527     }
528
529     getLog().info("Gathered hibernate-configuration (turn on debugging for details):");
530     for (Entry<Object,Object> entry : properties.entrySet())
531       getLog().info("  " + entry.getKey() + " = " + entry.getValue());
532
533     Connection connection = null;
534     try
535     {
536       /**
537        * The connection must be established outside of hibernate, because
538        * hibernate does not use the context-classloader of the current
539        * thread and, hence, would not be able to resolve the driver-class!
540        */
541       switch (target)
542       {
543         case EXPORT:
544         case BOTH:
545           switch (type)
546           {
547             case CREATE:
548             case DROP:
549             case BOTH:
550               Class driverClass = classLoader.loadClass(driverClassName);
551               getLog().debug("Registering JDBC-driver " + driverClass.getName());
552               DriverManager.registerDriver(new DriverProxy((Driver)driverClass.newInstance()));
553               getLog().debug("Opening JDBC-connection to " + url + " as " + username + " with password " + password);
554               connection = DriverManager.getConnection(url, username, password);
555           }
556       }
557     }
558     catch (ClassNotFoundException e)
559     {
560       getLog().error("Dependency for driver-class " + driverClassName + " is missing!");
561       throw new MojoExecutionException(e.getMessage());
562     }
563     catch (Exception e)
564     {
565       getLog().error("Cannot establish connection to database!");
566       Enumeration<Driver> drivers = DriverManager.getDrivers();
567       if (!drivers.hasMoreElements())
568         getLog().error("No drivers registered!");
569       while (drivers.hasMoreElements())
570         getLog().debug("Driver: " + drivers.nextElement());
571       throw new MojoExecutionException(e.getMessage());
572     }
573
574     ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
575     MavenLogAppender.startPluginLog(this);
576     try
577     {
578       /**
579        * Change class-loader of current thread, so that hibernate can
580        * see all dependencies!
581        */
582       Thread.currentThread().setContextClassLoader(classLoader);
583
584       SchemaExport export = new SchemaExport(config, connection);
585       export.setOutputFile(outputFile);
586       export.setDelimiter(delimiter);
587       export.setFormat(format);
588       export.execute(target, type);
589
590       for (Object exception : export.getExceptions())
591         getLog().debug(exception.toString());
592     }
593     finally
594     {
595       /** Stop Log-Capturing */
596       MavenLogAppender.endPluginLog(this);
597
598       /** Restore the old class-loader (TODO: is this really necessary?) */
599       Thread.currentThread().setContextClassLoader(contextClassLoader);
600
601       /** Close the connection */
602       try
603       {
604         connection.close();
605       }
606       catch (SQLException e)
607       {
608         getLog().error("Error while closing connection: " + e.getMessage());
609       }
610     }
611
612     /** Write md5-sums for annotated classes to file */
613     try
614     {
615       FileOutputStream fos = new FileOutputStream(saved);
616       ObjectOutputStream oos = new ObjectOutputStream(fos);
617       oos.writeObject(md5s);
618       oos.close();
619       fos.close();
620     }
621     catch (Exception e)
622     {
623       getLog().error("Cannot write md5-sums to file: " + e);
624     }
625   }
626
627   /**
628    * Needed, because DriverManager won't pick up drivers, that were not
629    * loaded by the system-classloader!
630    * See:
631    * http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-fromodifiedm-an-arbitrary-location
632    */
633   static final class DriverProxy implements Driver
634   {
635     private final Driver target;
636
637     DriverProxy(Driver target)
638     {
639       if (target == null)
640         throw new NullPointerException();
641       this.target = target;
642     }
643
644     public java.sql.Driver getTarget()
645     {
646       return target;
647     }
648
649     @Override
650     public boolean acceptsURL(String url) throws SQLException
651     {
652       return target.acceptsURL(url);
653     }
654
655     @Override
656     public java.sql.Connection connect(
657         String url,
658         java.util.Properties info
659       )
660       throws
661         SQLException
662     {
663       return target.connect(url, info);
664     }
665
666     @Override
667     public int getMajorVersion()
668     {
669       return target.getMajorVersion();
670     }
671
672     @Override
673     public int getMinorVersion()
674     {
675       return target.getMinorVersion();
676     }
677
678     @Override
679     public DriverPropertyInfo[] getPropertyInfo(
680         String url,
681         Properties info
682       )
683       throws
684         SQLException
685     {
686       return target.getPropertyInfo(url, info);
687     }
688
689     @Override
690     public boolean jdbcCompliant()
691     {
692       return target.jdbcCompliant();
693     }
694
695     /**
696      * This Method cannot be annotated with @Override, becaus the plugin
697      * will not compile then under Java 1.6!
698      */
699     public Logger getParentLogger() throws SQLFeatureNotSupportedException
700     {
701       throw new SQLFeatureNotSupportedException("Not supported, for backward-compatibility with Java 1.6");
702     }
703
704     @Override
705     public String toString()
706     {
707       return "Proxy: " + target;
708     }
709
710     @Override
711     public int hashCode()
712     {
713       return target.hashCode();
714     }
715
716     @Override
717     public boolean equals(Object obj)
718     {
719       if (!(obj instanceof DriverProxy))
720         return false;
721       DriverProxy other = (DriverProxy) obj;
722       return this.target.equals(other.target);
723     }
724   }
725 }