Hibernate configuration is gathered from class-path by default
[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.FileNotFoundException;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.ObjectInputStream;
27 import java.io.ObjectOutputStream;
28 import java.math.BigInteger;
29 import java.net.URL;
30 import java.net.URLClassLoader;
31 import java.security.MessageDigest;
32 import java.security.NoSuchAlgorithmException;
33 import java.sql.Driver;
34 import java.sql.DriverPropertyInfo;
35 import java.sql.SQLException;
36 import java.sql.SQLFeatureNotSupportedException;
37 import java.util.Collections;
38 import java.util.Comparator;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.LinkedHashSet;
42 import java.util.LinkedList;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Map.Entry;
46 import java.util.Properties;
47 import java.util.Set;
48 import java.util.TreeSet;
49 import java.util.logging.Logger;
50 import java.util.regex.Matcher;
51 import java.util.regex.Pattern;
52 import javax.persistence.Embeddable;
53 import javax.persistence.Entity;
54 import javax.persistence.MappedSuperclass;
55 import javax.persistence.spi.PersistenceUnitTransactionType;
56 import org.apache.maven.artifact.Artifact;
57 import org.apache.maven.model.Resource;
58 import org.apache.maven.plugin.AbstractMojo;
59 import org.apache.maven.plugin.MojoExecutionException;
60 import org.apache.maven.plugin.MojoFailureException;
61 import org.apache.maven.project.MavenProject;
62 import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
63 import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
64 import org.hibernate.boot.registry.classloading.spi.ClassLoadingException;
65 import org.hibernate.boot.registry.internal.StandardServiceRegistryImpl;
66 import org.hibernate.cfg.Environment;
67 import org.hibernate.cfg.NamingStrategy;
68 import org.hibernate.envers.configuration.spi.AuditConfiguration;
69 import org.hibernate.internal.util.config.ConfigurationHelper;
70 import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
71 import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
72 import org.hibernate.jpa.boot.spi.ProviderChecker;
73 import org.hibernate.tool.hbm2ddl.SchemaExport;
74 import org.hibernate.tool.hbm2ddl.SchemaExport.Type;
75 import org.hibernate.tool.hbm2ddl.Target;
76 import org.scannotation.AnnotationDB;
77
78
79 /**
80  * Goal which extracts the hibernate-mapping-configuration and
81  * exports an according SQL-database-schema.
82  *
83  * @goal export
84  * @phase process-classes
85  * @threadSafe
86  * @requiresDependencyResolution runtime
87  */
88 public class Hbm2DdlMojo extends AbstractMojo
89 {
90   public final static String EXPORT_SKIPPED_PROPERTY = "hibernate.export.skipped";
91
92   public final static String DRIVER_CLASS = "hibernate.connection.driver_class";
93   public final static String URL = "hibernate.connection.url";
94   public final static String USERNAME = "hibernate.connection.username";
95   public final static String PASSWORD = "hibernate.connection.password";
96   public final static String DIALECT = "hibernate.dialect";
97   public final static String NAMING_STRATEGY="hibernate.ejb.naming_strategy";
98   public final static String ENVERS = "hibernate.export.envers";
99
100   public final static String MD5S = "hibernate4-generatedschema.md5s";
101
102   private final static Pattern split = Pattern.compile("[^,\\s]+");
103
104
105   /**
106    * The maven project.
107    * <p>
108    * Only needed internally.
109    *
110    * @parameter property="project"
111    * @required
112    * @readonly
113    */
114   private MavenProject project;
115
116   /**
117    * Build-directory.
118    * <p>
119    * Only needed internally.
120    *
121    * @parameter property="project.build.directory"
122    * @required
123    * @readonly
124    */
125   private String buildDirectory;
126
127   /**
128    * Classes-Directory to scan.
129    * <p>
130    * This parameter defaults to the maven build-output-directory for classes.
131    * Additionally, all dependencies are scanned for annotated classes.
132    *
133    * @parameter property="project.build.outputDirectory"
134    * @since 1.0
135    */
136   private String outputDirectory;
137
138   /**
139    * Whether to scan test-classes too, or not.
140    * <p>
141    * If this parameter is set to <code>true</code> the test-classes of the
142    * artifact will be scanned for hibernate-annotated classes additionally.
143    *
144    * @parameter property="hibernate.export.scan_testclasses" default-value="false"
145    * @since 1.0.1
146    */
147   private boolean scanTestClasses;
148
149   /**
150    * Dependency-Scopes, that should be scanned for annotated classes.
151    * <p>
152    * By default, only dependencies in the scope <code>compile</code> are
153    * scanned for annotated classes. Multiple scopes can be seperated by
154    * white space or commas.
155    * <p>
156    * If you do not want any dependencies to be scanned for annotated
157    * classes, set this parameter to <code>none</code>.
158    * <p>
159    * The plugin does not scan for annotated classes in transitive
160    * dependencies. If some of your annotated classes are hidden in a
161    * transitive dependency, you can simply add that dependency explicitly.
162    *
163    * @parameter property="hibernate.export.scan_dependencies" default-value="compile"
164    * @since 1.0.3
165    */
166   private String scanDependencies;
167
168   /**
169    * Test-Classes-Directory to scan.
170    * <p>
171    * This parameter defaults to the maven build-output-directory for
172    * test-classes.
173    * <p>
174    * This parameter is only used, when <code>scanTestClasses</code> is set
175    * to <code>true</code>!
176    *
177    * @parameter property="project.build.testOutputDirectory"
178    * @since 1.0.2
179    */
180   private String testOutputDirectory;
181
182   /**
183    * Skip execution
184    * <p>
185    * If set to <code>true</code>, the execution is skipped.
186    * <p>
187    * A skipped execution is signaled via the maven-property
188    * <code>${hibernate.export.skipped}</code>.
189    * <p>
190    * The execution is skipped automatically, if no modified or newly added
191    * annotated classes are found and the dialect was not changed.
192    *
193    * @parameter property="hibernate.skip" default-value="${maven.test.skip}"
194    * @since 1.0
195    */
196   private boolean skip;
197
198   /**
199    * Force execution
200    * <p>
201    * Force execution, even if no modified or newly added annotated classes
202    * where found and the dialect was not changed.
203    * <p>
204    * <code>skip</code> takes precedence over <code>force</code>.
205    *
206    * @parameter property="hibernate.export.force" default-value="false"
207    * @since 1.0
208    */
209   private boolean force;
210
211   /**
212    * SQL-Driver name.
213    *
214    * @parameter property="hibernate.connection.driver_class"
215    * @since 1.0
216    */
217   private String driverClassName;
218
219   /**
220    * Database URL.
221    *
222    * @parameter property="hibernate.connection.url"
223    * @since 1.0
224    */
225   private String url;
226
227   /**
228    * Database username
229    *
230    * @parameter property="hibernate.connection.username"
231    * @since 1.0
232    */
233   private String username;
234
235   /**
236    * Database password
237    *
238    * @parameter property="hibernate.connection.password"
239    * @since 1.0
240    */
241   private String password;
242
243   /**
244    * Hibernate dialect.
245    *
246    * @parameter property="hibernate.dialect"
247    * @since 1.0
248    */
249   private String hibernateDialect;
250
251   /**
252    * Hibernate Naming Strategy
253    *
254    * @parameter property="hibernate.ejb.naming_strategy"
255    * @since 1.0.2
256    */
257   private String hibernateNamingStrategy;
258
259   /**
260    * Path to Hibernate properties file.
261    * If this parameter is not set the plugin will try to load the configuration
262    * from a file <code>hibernate.properties</code> on the classpath. The
263    * test-classpath takes precedence.
264    *
265    * @parameter
266    * @since 1.0
267    */
268   private String hibernateProperties;
269
270   /**
271    * Path to Hibernate configuration file (.cfg.xml).
272    * Settings in this file will overwrite settings in the properties file.
273    * If this parameter is not set the plugin will try to load the configuration
274    * from a file <code>hibernate.cfg.xml</code> on the classpath. The
275    * test-classpath takes precedence.
276    *
277    * @parameter
278    * @since 1.1.0
279    */
280   private String hibernateConfig;
281
282   /**
283    * Name of the persistence-unit.
284    * If there is only one persistence-unit available, that unit will be used
285    * automatically.
286    * Settings in this file will overwrite settings in the properties or the
287    * configuration file.
288    *
289    * @parameter
290    * @since 1.1.0
291    */
292   private String persistenceUnit;
293
294   /**
295    * List of Hibernate-Mapping-Files (XML).
296    * Multiple files can be separated with white-spaces and/or commas.
297    *
298    * @parameter property="hibernate.mapping"
299    * @since 1.0.2
300    */
301   private String hibernateMapping;
302
303   /**
304    * Target of execution:
305    * <ul>
306    *   <li><strong>NONE</strong> only export schema to SQL-script (forces execution, signals skip)</li>
307    *   <li><strong>EXPORT</strong> create database (<strong>DEFAULT!</strong>). forces execution, signals skip)</li>
308    *   <li><strong>SCRIPT</strong> export schema to SQL-script and print it to STDOUT</li>
309    *   <li><strong>BOTH</strong></li>
310    * </ul>
311    *
312    * A database connection is only needed for EXPORT and BOTH, but a
313    * Hibernate-Dialect must always be chosen.
314    *
315    * @parameter property="hibernate.export.target" default-value="EXPORT"
316    * @since 1.0
317    */
318   private String target;
319
320   /**
321    * Type of execution.
322    * <ul>
323    *   <li><strong>NONE</strong> do nothing - just validate the configuration</li>
324    *   <li><strong>CREATE</strong> create database-schema</li>
325    *   <li><strong>DROP</strong> drop database-schema</li>
326    *   <li><strong>BOTH</strong> (<strong>DEFAULT!</strong>)</li>
327    * </ul>
328    *
329    * If NONE is choosen, no databaseconnection is needed.
330    *
331    * @parameter property="hibernate.export.type" default-value="BOTH"
332    * @since 1.0
333    */
334   private String type;
335
336   /**
337    * Output file.
338    *
339    * @parameter property="hibernate.export.schema.filename" default-value="${project.build.directory}/schema.sql"
340    * @since 1.0
341    */
342   private String outputFile;
343
344   /**
345    * Delimiter in output-file.
346    *
347    * @parameter property="hibernate.export.schema.delimiter" default-value=";"
348    * @since 1.0
349    */
350   private String delimiter;
351
352   /**
353    * Format output-file.
354    *
355    * @parameter property="hibernate.export.schema.format" default-value="true"
356    * @since 1.0
357    */
358   private boolean format;
359
360   /**
361    * Generate envers schema for auditing tables.
362    *
363    * @parameter property="hibernate.export.envers" default-value="true"
364    * @since 1.0.3
365    */
366   private boolean envers;
367
368
369   @Override
370   public void execute()
371     throws
372       MojoFailureException,
373       MojoExecutionException
374   {
375     if (skip)
376     {
377       getLog().info("Execution of hibernate4-maven-plugin:export was skipped!");
378       project.getProperties().setProperty(EXPORT_SKIPPED_PROPERTY, "true");
379       return;
380     }
381
382     Map<String,String> md5s;
383     boolean modified = false;
384     File saved = new File(buildDirectory + File.separator + MD5S);
385
386     if (saved.isFile() && saved.length() > 0)
387     {
388       try
389       {
390         FileInputStream fis = new FileInputStream(saved);
391         ObjectInputStream ois = new ObjectInputStream(fis);
392         md5s = (HashMap<String,String>)ois.readObject();
393         ois.close();
394       }
395       catch (Exception e)
396       {
397         md5s = new HashMap<String,String>();
398         getLog().warn("Cannot read timestamps from saved: " + e);
399       }
400     }
401     else
402     {
403       md5s = new HashMap<String,String>();
404       try
405       {
406         saved.createNewFile();
407       }
408       catch (IOException e)
409       {
410         getLog().debug("Cannot create file \"" + saved.getPath() + "\" for timestamps: " + e);
411       }
412     }
413
414     URLClassLoader classLoader = null;
415     try
416     {
417       getLog().debug("Creating ClassLoader for project-dependencies...");
418       List<String> classpathFiles = project.getCompileClasspathElements();
419       if (scanTestClasses)
420         classpathFiles.addAll(project.getTestClasspathElements());
421       List<URL> urls = new LinkedList<URL>();
422       File file;
423       file = new File(testOutputDirectory);
424       if (!file.exists())
425       {
426         getLog().info("creating test-output-directory: " + testOutputDirectory);
427         file.mkdirs();
428       }
429       urls.add(file.toURI().toURL());
430       file = new File(outputDirectory);
431       if (!file.exists())
432       {
433         getLog().info("creating output-directory: " + outputDirectory);
434         file.mkdirs();
435       }
436       urls.add(file.toURI().toURL());
437       for (String pathElement : classpathFiles)
438       {
439         getLog().debug("Dependency: " + pathElement);
440         urls.add(new File(pathElement).toURI().toURL());
441       }
442       classLoader =
443           new URLClassLoader(
444               urls.toArray(new URL[urls.size()]),
445               getClass().getClassLoader()
446               );
447     }
448     catch (Exception e)
449     {
450       getLog().error("Error while creating ClassLoader!", e);
451       throw new MojoExecutionException(e.getMessage());
452     }
453
454     Set<Class<?>> classes =
455         new TreeSet<Class<?>>(
456             new Comparator<Class<?>>() {
457               @Override
458               public int compare(Class<?> a, Class<?> b)
459               {
460                 return a.getName().compareTo(b.getName());
461               }
462             }
463           );
464
465     try
466     {
467       AnnotationDB db = new AnnotationDB();
468       File dir = new File(outputDirectory);
469       if (dir.exists())
470       {
471         getLog().info("Scanning directory " + outputDirectory + " for annotated classes...");
472         URL dirUrl = dir.toURI().toURL();
473         db.scanArchives(dirUrl);
474       }
475       if (scanTestClasses)
476       {
477         dir = new File(testOutputDirectory);
478         if (dir.exists())
479         {
480           getLog().info("Scanning directory " + testOutputDirectory + " for annotated classes...");
481           URL dirUrl = dir.toURI().toURL();
482           db.scanArchives(dirUrl);
483         }
484       }
485       if (scanDependencies != null)
486       {
487         Matcher matcher = split.matcher(scanDependencies);
488         while (matcher.find())
489         {
490           getLog().info("Scanning dependencies for scope " + matcher.group());
491           for (Artifact artifact : project.getDependencyArtifacts())
492           {
493             if (!artifact.getScope().equalsIgnoreCase(matcher.group()))
494               continue;
495             if (artifact.getFile() == null)
496             {
497               getLog().warn(
498                   "Cannot scan dependency " +
499                   artifact.getId() +
500                   ": no JAR-file available!"
501                   );
502               continue;
503             }
504             getLog().info(
505                 "Scanning dependency " +
506                 artifact.getId() +
507                 " for annotated classes..."
508                 );
509             db.scanArchives(artifact.getFile().toURI().toURL());
510           }
511         }
512       }
513
514       Set<String> classNames = new HashSet<String>();
515       if (db.getAnnotationIndex().containsKey(Entity.class.getName()))
516         classNames.addAll(db.getAnnotationIndex().get(Entity.class.getName()));
517       if (db.getAnnotationIndex().containsKey(MappedSuperclass.class.getName()))
518         classNames.addAll(db.getAnnotationIndex().get(MappedSuperclass.class.getName()));
519       if (db.getAnnotationIndex().containsKey(Embeddable.class.getName()))
520         classNames.addAll(db.getAnnotationIndex().get(Embeddable.class.getName()));
521
522       MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
523       for (String name : classNames)
524       {
525         Class<?> annotatedClass = classLoader.loadClass(name);
526         classes.add(annotatedClass);
527         String resourceName = annotatedClass.getName();
528         resourceName = resourceName.substring(resourceName.lastIndexOf(".") + 1, resourceName.length()) + ".class";
529         InputStream is =
530             annotatedClass
531                 .getResourceAsStream(resourceName);
532         byte[] buffer = new byte[1024*4]; // copy data in 4MB-chunks
533         int i;
534         while((i = is.read(buffer)) > -1)
535           digest.update(buffer, 0, i);
536         is.close();
537         byte[] bytes = digest.digest();
538         BigInteger bi = new BigInteger(1, bytes);
539         String newMd5 = String.format("%0" + (bytes.length << 1) + "x", bi);
540         String oldMd5 = !md5s.containsKey(name) ? "" : md5s.get(name);
541         if (!newMd5.equals(oldMd5))
542         {
543           getLog().debug("Found new or modified annotated class: " + name);
544           modified = true;
545           md5s.put(name, newMd5);
546         }
547         else
548         {
549           getLog().debug(oldMd5 + " -> class unchanged: " + name);
550         }
551       }
552     }
553     catch (ClassNotFoundException e)
554     {
555       getLog().error("Error while adding annotated classes!", e);
556       throw new MojoExecutionException(e.getMessage());
557     }
558     catch (Exception e)
559     {
560       getLog().error("Error while scanning!", e);
561       throw new MojoFailureException(e.getMessage());
562     }
563
564
565     ValidationConfiguration config = new ValidationConfiguration();
566     // Clear unused system-properties
567     config.setProperties(new Properties());
568
569
570     ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
571     StandardServiceRegistryImpl registry = null;
572     MavenLogAppender.startPluginLog(this);
573
574     try
575     {
576       /**
577        * Change class-loader of current thread, so that hibernate can
578        * see all dependencies!
579        */
580       Thread.currentThread().setContextClassLoader(classLoader);
581
582
583       /** Try to read configuration from properties-file */
584       try
585       {
586         if (hibernateProperties == null)
587         {
588           URL url = classLoader.findResource("hibernate.properties");
589           if (url == null)
590           {
591             getLog().info("No hibernate.properties on the classpath!");
592           }
593           else
594           {
595             getLog().info("Reading settings from hibernate.properties on the classpath.");
596             Properties properties = new Properties();
597             properties.load(url.openStream());
598             config.setProperties(properties);
599           }
600         }
601         else
602         {
603           File file = new File(hibernateProperties);
604           if (file.exists())
605           {
606             getLog().info("Reading settings from file " + hibernateProperties + "...");
607             Properties properties = new Properties();
608             properties.load(new FileInputStream(file));
609             config.setProperties(properties);
610           }
611           else
612             getLog().info("No hibernate-properties-file found! (Checked path: " + hibernateProperties + ")");
613         }
614       }
615       catch (IOException e)
616       {
617         getLog().error("Error while reading properties!", e);
618         throw new MojoExecutionException(e.getMessage());
619       }
620
621       /** Try to read configuration from configuration-file */
622       try
623       {
624         if (hibernateConfig == null)
625         {
626           URL url = classLoader.findResource("hibernate.cfg.xml");
627           if (url == null)
628           {
629             getLog().info("No hibernate.cfg.xml on the classpath!");
630           }
631           else
632           {
633             getLog().info("Reading settings from hibernate.cfg.xml on the classpath.");
634             config.configure(url);
635           }
636         }
637         else
638         {
639           File file = new File(hibernateConfig);
640           if (file.exists())
641           {
642             getLog().info("Reading configuration from file " + hibernateConfig + "...");
643             config.configure(file);
644           }
645           else
646             getLog().info("No hibernate-configuration-file found! (Checked path: " + hibernateConfig + ")");
647         }
648       }
649       catch (Exception e)
650       {
651         getLog().error("Error while reading configuration!", e);
652         throw new MojoExecutionException(e.getMessage());
653       }
654
655       ParsedPersistenceXmlDescriptor persistenceUnitDescriptor =
656           getPersistenceUnitDescriptor(
657               persistenceUnit,
658               config.getProperties(),
659               new MavenProjectClassLoaderService(classLoader)
660               );
661       if (persistenceUnitDescriptor != null)
662         config.setProperties(persistenceUnitDescriptor.getProperties());
663
664       /** Overwrite values from properties-file or set, if given */
665       if (driverClassName != null)
666       {
667         if (config.getProperties().containsKey(DRIVER_CLASS))
668           getLog().debug(
669               "Overwriting property " +
670               DRIVER_CLASS + "=" + config.getProperty(DRIVER_CLASS) +
671               " with the value " + driverClassName
672             );
673         else
674           getLog().debug("Using the value " + driverClassName);
675         config.setProperty(DRIVER_CLASS, driverClassName);
676       }
677       if (url != null)
678       {
679         if (config.getProperties().containsKey(URL))
680           getLog().debug(
681               "Overwriting property " +
682               URL + "=" + config.getProperty(URL) +
683               " with the value " + url
684             );
685         else
686           getLog().debug("Using the value " + url);
687         config.setProperty(URL, url);
688       }
689       if (username != null)
690       {
691         if (config.getProperties().containsKey(USERNAME))
692           getLog().debug(
693               "Overwriting property " +
694               USERNAME + "=" + config.getProperty(USERNAME) +
695               " with the value " + username
696             );
697         else
698           getLog().debug("Using the value " + username);
699         config.setProperty(USERNAME, username);
700       }
701       if (password != null)
702       {
703         if (config.getProperties().containsKey(PASSWORD))
704           getLog().debug(
705               "Overwriting property " +
706               PASSWORD + "=" + config.getProperty(PASSWORD) +
707               " with value " + password
708             );
709         else
710           getLog().debug("Using value " + password + " for property " + PASSWORD);
711         config.setProperty(PASSWORD, password);
712       }
713       if (hibernateDialect != null)
714       {
715         if (config.getProperties().containsKey(DIALECT))
716           getLog().debug(
717               "Overwriting property " +
718               DIALECT + "=" + config.getProperty(DIALECT) +
719               " with value " + hibernateDialect
720             );
721         else
722           getLog().debug(
723               "Using value " + hibernateDialect + " for property " + DIALECT
724               );
725         config.setProperty(DIALECT, hibernateDialect);
726       }
727       if ( hibernateNamingStrategy != null )
728       {
729         if ( config.getProperties().contains(NAMING_STRATEGY))
730           getLog().debug(
731               "Overwriting property " +
732               NAMING_STRATEGY + "=" + config.getProperty(NAMING_STRATEGY) +
733               " with value " + hibernateNamingStrategy
734              );
735         else
736           getLog().debug(
737               "Using value " + hibernateNamingStrategy + " for property " +
738               NAMING_STRATEGY
739               );
740         config.setProperty(NAMING_STRATEGY, hibernateNamingStrategy);
741       }
742
743       /** The generated SQL varies with the dialect! */
744       if (md5s.containsKey(DIALECT))
745       {
746         String dialect = config.getProperty(DIALECT);
747         if (md5s.get(DIALECT).equals(dialect))
748           getLog().debug("SQL-dialect unchanged.");
749         else
750         {
751           modified = true;
752           if (dialect == null)
753           {
754             getLog().debug("SQL-dialect was unset.");
755             md5s.remove(DIALECT);
756           }
757           else
758           {
759             getLog().debug("SQL-dialect changed: " + dialect);
760             md5s.put(DIALECT, dialect);
761           }
762         }
763       }
764       else
765       {
766         String dialect = config.getProperty(DIALECT);
767         if (dialect != null)
768         {
769           modified = true;
770           md5s.put(DIALECT, config.getProperty(DIALECT));
771         }
772       }
773
774       /** The generated SQL varies with the envers-configuration */
775       if (md5s.get(ENVERS) != null)
776       {
777         if (md5s.get(ENVERS).equals(Boolean.toString(envers)))
778           getLog().debug("Envers-Configuration unchanged. Enabled: " + envers);
779         else
780         {
781           getLog().debug("Envers-Configuration changed. Enabled: " + envers);
782           modified = true;
783           md5s.put(ENVERS, Boolean.toString(envers));
784         }
785       }
786       else
787       {
788         modified = true;
789         md5s.put(ENVERS, Boolean.toString(envers));
790       }
791
792       if (config.getProperties().isEmpty())
793       {
794         getLog().error("No properties set!");
795         throw new MojoFailureException("Hibernate configuration is missing!");
796       }
797
798       getLog().info("Gathered hibernate-configuration (turn on debugging for details):");
799       for (Entry<Object,Object> entry : config.getProperties().entrySet())
800         getLog().info("  " + entry.getKey() + " = " + entry.getValue());
801
802
803       getLog().debug("Adding explicitly configured mappings...");
804       if (hibernateMapping != null)
805       {
806         try
807         {
808           MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
809           for (String filename : hibernateMapping.split("[\\s,]+"))
810           {
811             // First try the filename as absolute/relative path
812             File file = new File(filename);
813             if (!file.exists())
814             {
815               // If the file was not found, search for it in the resource-directories
816               for (Resource resource : project.getResources())
817               {
818                 file = new File(resource.getDirectory() + File.separator + filename);
819                 if (file.exists())
820                   break;
821               }
822             }
823             if (file != null && file.exists())
824             {
825               InputStream is = new FileInputStream(file);
826               byte[] buffer = new byte[1024*4]; // copy data in 4MB-chunks
827               int i;
828               while((i = is.read(buffer)) > -1)
829                 digest.update(buffer, 0, i);
830               is.close();
831               byte[] bytes = digest.digest();
832               BigInteger bi = new BigInteger(1, bytes);
833               String newMd5 = String.format("%0" + (bytes.length << 1) + "x", bi);
834               String oldMd5 = !md5s.containsKey(filename) ? "" : md5s.get(filename);
835               if (!newMd5.equals(oldMd5))
836               {
837                 getLog().debug("Found new or modified mapping-file: " + filename);
838                 modified = true;
839                 md5s.put(filename, newMd5);
840               }
841               else
842               {
843                 getLog().debug(oldMd5 + " -> mapping-file unchanged: " + filename);
844               }
845               getLog().debug("Adding mappings from XML-configurationfile: " + file);
846               config.addFile(file);
847             }
848             else
849               throw new MojoFailureException("File " + filename + " could not be found in any of the configured resource-directories!");
850           }
851         }
852         catch (NoSuchAlgorithmException e)
853         {
854           throw new MojoFailureException("Cannot calculate MD5 sums!", e);
855         }
856         catch (FileNotFoundException e)
857         {
858           throw new MojoFailureException("Cannot calculate MD5 sums!", e);
859         }
860         catch (IOException e)
861         {
862           throw new MojoFailureException("Cannot calculate MD5 sums!", e);
863         }
864       }
865
866       getLog().debug("Adding annotated classes to hibernate-mapping-configuration...");
867       // build annotated packages
868       Set<String> packages = new HashSet<String>();
869       for (Class<?> annotatedClass : classes)
870       {
871         String packageName = annotatedClass.getPackage().getName();
872         if (!packages.contains(packageName))
873         {
874           getLog().debug("Add package " + packageName);
875           packages.add(packageName);
876           config.addPackage(packageName);
877           getLog().debug("type definintions" + config.getTypeDefs());
878         }
879         getLog().debug("Class " + annotatedClass);
880         config.addAnnotatedClass(annotatedClass);
881       }
882
883       Target target = null;
884       try
885       {
886         target = Target.valueOf(this.target.toUpperCase());
887       }
888       catch (IllegalArgumentException e)
889       {
890         getLog().error("Invalid value for configuration-option \"target\": " + this.target);
891         getLog().error("Valid values are: NONE, SCRIPT, EXPORT, BOTH");
892         throw new MojoExecutionException("Invalid value for configuration-option \"target\"");
893       }
894       Type type = null;
895       try
896       {
897         type = Type.valueOf(this.type.toUpperCase());
898       }
899       catch (IllegalArgumentException e)
900       {
901         getLog().error("Invalid value for configuration-option \"type\": " + this.type);
902         getLog().error("Valid values are: NONE, CREATE, DROP, BOTH");
903         throw new MojoExecutionException("Invalid value for configuration-option \"type\"");
904       }
905
906
907       if (config.getProperty(DIALECT) == null)
908         throw new MojoFailureException("hibernate-dialect must be set!");
909
910
911       if (target.equals(Target.SCRIPT) || target.equals(Target.NONE))
912       {
913         project.getProperties().setProperty(EXPORT_SKIPPED_PROPERTY, "true");
914       }
915       if (
916           !modified
917           && !target.equals(Target.SCRIPT)
918           && !target.equals(Target.NONE)
919           && !force
920         )
921       {
922         getLog().info("No modified annotated classes or mapping-files found and dialect unchanged.");
923         getLog().info("Skipping schema generation!");
924         project.getProperties().setProperty(EXPORT_SKIPPED_PROPERTY, "true");
925         return;
926       }
927
928
929       if ( config.getProperties().containsKey(NAMING_STRATEGY))
930       {
931         String namingStrategy = config.getProperty(NAMING_STRATEGY);
932         getLog().debug("Explicitly set NamingStrategy: " + namingStrategy);
933         try
934         {
935           @SuppressWarnings("unchecked")
936           Class<NamingStrategy> namingStrategyClass = (Class<NamingStrategy>) Class.forName(namingStrategy);
937           config.setNamingStrategy(namingStrategyClass.newInstance());
938         }
939         catch (Exception e)
940         {
941           getLog().error("Error setting NamingStrategy", e);
942           throw new MojoExecutionException(e.getMessage());
943         }
944       }
945
946
947       Environment.verifyProperties(config.getProperties());
948       ConfigurationHelper.resolvePlaceHolders(config.getProperties());
949       registry =
950           (StandardServiceRegistryImpl)
951           new StandardServiceRegistryBuilder()
952               .applySettings(config.getProperties())
953               .build();
954
955       config.buildMappings();
956
957       if (envers)
958       {
959         getLog().info("Automatic auditing via hibernate-envers enabled!");
960         AuditConfiguration.getFor(config);
961       }
962
963       SchemaExport export = new SchemaExport(registry, config);
964       export.setDelimiter(delimiter);
965       export.setFormat(format);
966
967       File outF = new File(outputFile);
968
969       if (!outF.isAbsolute())
970       {
971         // Interpret relative file path relative to build directory
972         outF = new File(buildDirectory, outputFile);
973         getLog().info("Adjusted relative path, resulting path is " + outF.getPath());
974       }
975
976       // Ensure that directory path for specified file exists
977       File outFileParentDir = outF.getParentFile();
978       if (null != outFileParentDir && !outFileParentDir.exists())
979       {
980         try
981         {
982           getLog().info("Creating directory path for output file:" + outFileParentDir.getPath());
983           outFileParentDir.mkdirs();
984         }
985         catch (Exception e)
986         {
987           getLog().error("Error creating directory path for output file: " + e.getLocalizedMessage());
988         }
989       }
990
991       export.setOutputFile(outF.getPath());
992       export.execute(target, type);
993
994       for (Object exception : export.getExceptions())
995         getLog().debug(exception.toString());
996     }
997     finally
998     {
999       /** Stop Log-Capturing */
1000       MavenLogAppender.endPluginLog(this);
1001
1002       /** Restore the old class-loader (TODO: is this really necessary?) */
1003       Thread.currentThread().setContextClassLoader(contextClassLoader);
1004
1005       if (registry != null)
1006         registry.destroy();
1007     }
1008
1009     /** Write md5-sums for annotated classes to file */
1010     try
1011     {
1012       FileOutputStream fos = new FileOutputStream(saved);
1013       ObjectOutputStream oos = new ObjectOutputStream(fos);
1014       oos.writeObject(md5s);
1015       oos.close();
1016       fos.close();
1017     }
1018     catch (Exception e)
1019     {
1020       getLog().error("Cannot write md5-sums to file: " + e);
1021     }
1022   }
1023
1024   private ParsedPersistenceXmlDescriptor getPersistenceUnitDescriptor(
1025       String name,
1026       Properties properties,
1027       ClassLoaderService loader
1028       )
1029       throws
1030         MojoFailureException
1031   {
1032     PersistenceXmlParser parser =
1033         new PersistenceXmlParser(
1034             loader,
1035             PersistenceUnitTransactionType.RESOURCE_LOCAL
1036              );
1037
1038     List<ParsedPersistenceXmlDescriptor> units = parser.doResolve(properties);
1039
1040     if (name == null)
1041     {
1042       switch (units.size())
1043       {
1044         case 0:
1045           getLog().info("Found no META-INF/persistence.xml.");
1046           return null;
1047         case 1:
1048           getLog().info("Using persistence-unit " + units.get(0).getName());
1049           return units.get(0);
1050         default:
1051           getLog().warn("No name provided and multiple persistence units found:");
1052           for (ParsedPersistenceXmlDescriptor unit : units)
1053             getLog().warn(" - " + unit.getName());
1054           return null;
1055       }
1056
1057     }
1058
1059     for (ParsedPersistenceXmlDescriptor unit : units)
1060     {
1061       getLog().debug("Found persistence-unit " + unit.getName());
1062       if (!unit.getName().equals(name))
1063         continue;
1064
1065       // See if we (Hibernate) are the persistence provider
1066       if (!ProviderChecker.isProvider(unit, properties))
1067       {
1068         getLog().debug("Wrong provider: " + unit.getProviderClassName());
1069         continue;
1070       }
1071
1072       getLog().info("Using persistence-unit " + unit.getName());
1073       return unit;
1074     }
1075
1076     throw new MojoFailureException("Could not find persistence-unit " + name);
1077   }
1078
1079
1080   static final class MavenProjectClassLoaderService implements ClassLoaderService
1081   {
1082     final private ClassLoader loader;
1083
1084
1085     public MavenProjectClassLoaderService(ClassLoader loader)
1086     {
1087       this.loader = loader;
1088     }
1089
1090
1091     @Override
1092     public <T> Class<T> classForName(String name)
1093     {
1094       try
1095       {
1096         return (Class<T>)loader.loadClass(name);
1097       }
1098       catch (ClassNotFoundException e)
1099       {
1100         throw new ClassLoadingException( "Unable to load class [" + name + "]", e );
1101       }
1102     }
1103
1104     @Override
1105     public URL locateResource(String name)
1106     {
1107       return loader.getResource(name);
1108     }
1109
1110     @Override
1111     public InputStream locateResourceStream(String name)
1112     {
1113       return loader.getResourceAsStream(name);
1114     }
1115
1116     @Override
1117     public List<URL> locateResources(String name)
1118     {
1119       try
1120       {
1121         return Collections.list(loader.getResources(name));
1122       }
1123       catch (IOException e)
1124       {
1125         return Collections.EMPTY_LIST;
1126       }
1127     }
1128
1129     @Override
1130     public <S> LinkedHashSet<S> loadJavaServices(Class<S> serviceContract)
1131     {
1132       throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
1133     }
1134
1135     @Override
1136     public void stop() { }
1137
1138   }
1139
1140
1141   /**
1142    * Needed, because DriverManager won't pick up drivers, that were not
1143    * loaded by the system-classloader!
1144    * See:
1145    * http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-fromodifiedm-an-arbitrary-location
1146    */
1147   static final class DriverProxy implements Driver
1148   {
1149     private final Driver target;
1150
1151     DriverProxy(Driver target)
1152     {
1153       if (target == null)
1154         throw new NullPointerException();
1155       this.target = target;
1156     }
1157
1158     public java.sql.Driver getTarget()
1159     {
1160       return target;
1161     }
1162
1163     @Override
1164     public boolean acceptsURL(String url) throws SQLException
1165     {
1166       return target.acceptsURL(url);
1167     }
1168
1169     @Override
1170     public java.sql.Connection connect(
1171         String url,
1172         java.util.Properties info
1173       )
1174       throws
1175         SQLException
1176     {
1177       return target.connect(url, info);
1178     }
1179
1180     @Override
1181     public int getMajorVersion()
1182     {
1183       return target.getMajorVersion();
1184     }
1185
1186     @Override
1187     public int getMinorVersion()
1188     {
1189       return target.getMinorVersion();
1190     }
1191
1192     @Override
1193     public DriverPropertyInfo[] getPropertyInfo(
1194         String url,
1195         Properties info
1196       )
1197       throws
1198         SQLException
1199     {
1200       return target.getPropertyInfo(url, info);
1201     }
1202
1203     @Override
1204     public boolean jdbcCompliant()
1205     {
1206       return target.jdbcCompliant();
1207     }
1208
1209     /**
1210      * This Method cannot be annotated with @Override, becaus the plugin
1211      * will not compile then under Java 1.6!
1212      */
1213     public Logger getParentLogger() throws SQLFeatureNotSupportedException
1214     {
1215       throw new SQLFeatureNotSupportedException("Not supported, for backward-compatibility with Java 1.6");
1216     }
1217
1218     @Override
1219     public String toString()
1220     {
1221       return "Proxy: " + target;
1222     }
1223
1224     @Override
1225     public int hashCode()
1226     {
1227       return target.hashCode();
1228     }
1229
1230     @Override
1231     public boolean equals(Object obj)
1232     {
1233       if (!(obj instanceof DriverProxy))
1234         return false;
1235       DriverProxy other = (DriverProxy) obj;
1236       return this.target.equals(other.target);
1237     }
1238   }
1239 }