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