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