WIP
[hibernate4-maven-plugin] / src / main / java / de / juplo / plugins / hibernate / AbstractSchemaMojo.java
1 package de.juplo.plugins.hibernate;
2
3
4 import com.pyx4j.log4j.MavenLogAppender;
5 import java.io.File;
6 import java.io.FileInputStream;
7 import java.io.FileOutputStream;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.net.MalformedURLException;
11 import java.net.URL;
12 import java.security.NoSuchAlgorithmException;
13 import java.time.ZonedDateTime;
14 import java.util.Collections;
15 import java.util.HashSet;
16 import java.util.Iterator;
17 import java.util.LinkedHashSet;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Map.Entry;
21 import java.util.Properties;
22 import java.util.Set;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25 import javax.persistence.Embeddable;
26 import javax.persistence.Entity;
27 import javax.persistence.MappedSuperclass;
28 import javax.persistence.spi.PersistenceUnitTransactionType;
29 import org.apache.maven.artifact.Artifact;
30 import org.apache.maven.model.Resource;
31 import org.apache.maven.plugin.AbstractMojo;
32 import org.apache.maven.plugin.MojoExecutionException;
33 import org.apache.maven.plugin.MojoFailureException;
34 import org.apache.maven.project.MavenProject;
35 import org.hibernate.boot.MetadataBuilder;
36 import org.hibernate.boot.MetadataSources;
37 import org.hibernate.boot.cfgxml.internal.ConfigLoader;
38 import org.hibernate.boot.cfgxml.spi.LoadedConfig;
39 import org.hibernate.boot.cfgxml.spi.MappingReference;
40 import org.hibernate.boot.model.naming.ImplicitNamingStrategy;
41 import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
42 import org.hibernate.boot.registry.BootstrapServiceRegistry;
43 import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder;
44 import org.hibernate.boot.registry.StandardServiceRegistry;
45 import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
46 import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
47 import org.hibernate.boot.registry.classloading.spi.ClassLoadingException;
48 import org.hibernate.boot.registry.selector.spi.StrategySelector;
49 import org.hibernate.boot.spi.MetadataImplementor;
50 import static org.hibernate.cfg.AvailableSettings.DIALECT;
51 import static org.hibernate.cfg.AvailableSettings.DRIVER;
52 import static org.hibernate.cfg.AvailableSettings.FORMAT_SQL;
53 import static org.hibernate.cfg.AvailableSettings.HBM2DDL_DELIMITER;
54 import static org.hibernate.cfg.AvailableSettings.HBM2DLL_CREATE_NAMESPACES;
55 import static org.hibernate.cfg.AvailableSettings.IMPLICIT_NAMING_STRATEGY;
56 import static org.hibernate.cfg.AvailableSettings.JPA_JDBC_DRIVER;
57 import static org.hibernate.cfg.AvailableSettings.JPA_JDBC_PASSWORD;
58 import static org.hibernate.cfg.AvailableSettings.JPA_JDBC_URL;
59 import static org.hibernate.cfg.AvailableSettings.JPA_JDBC_USER;
60 import static org.hibernate.cfg.AvailableSettings.PASS;
61 import static org.hibernate.cfg.AvailableSettings.PHYSICAL_NAMING_STRATEGY;
62 import static org.hibernate.cfg.AvailableSettings.SHOW_SQL;
63 import static org.hibernate.cfg.AvailableSettings.USER;
64 import static org.hibernate.cfg.AvailableSettings.URL;
65 import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
66 import org.hibernate.internal.util.config.ConfigurationException;
67 import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
68 import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
69 import org.hibernate.tool.schema.internal.ExceptionHandlerCollectingImpl;
70 import org.scannotation.AnnotationDB;
71
72
73 /**
74  * Baseclass with common attributes and methods.
75  *
76  * @phase process-classes
77  * @threadSafe
78  * @requiresDependencyResolution runtime
79  */
80 public abstract class AbstractSchemaMojo extends AbstractMojo
81 {
82   public final static String EXECUTE = "hibernate.schema.execute";
83   public final static String OUTPUTDIRECTORY = "project.build.outputDirectory";
84   public final static String SCAN_CLASSES = "hibernate.schema.scan.classes";
85   public final static String SCAN_DEPENDENCIES = "hibernate.schema.scan.dependencies";
86   public final static String SCAN_TESTCLASSES = "hibernate.schema.scan.test_classes";
87   public final static String TEST_OUTPUTDIRECTORY = "project.build.testOutputDirectory";
88   public final static String SKIPPED = "hibernate.schema.skipped";
89   public final static String SCRIPT = "hibernate.schema.script";
90
91   private final static Pattern SPLIT = Pattern.compile("[^,\\s]+");
92
93   private final Set<String> packages = new HashSet<String>();
94
95
96   /**
97    * The maven project.
98    * <p>
99    * Only needed internally.
100    *
101    * @parameter property="project"
102    * @required
103    * @readonly
104    */
105   private MavenProject project;
106
107   /**
108    * Build-directory.
109    * <p>
110    * Only needed internally.
111    *
112    * @parameter property="project.build.directory"
113    * @required
114    * @readonly
115    */
116   private String buildDirectory;
117
118
119   /** Parameters to configure the genaration of the SQL *********************/
120
121   /**
122    * Excecute the generated SQL.
123    * If set to <code>false</code>, only the SQL-script is created and the
124    * database is not touched.
125    * <p>
126    * <strong>Important:</strong>
127    * This configuration value can only be configured through the
128    * <code>pom.xml</code>, or by the definition of a system-property, because
129    * it is not known by Hibernate nor JPA and, hence, not picked up from
130    * their configuration!
131    *
132    * @parameter property="hibernate.schema.execute" default-value="true"
133    * @since 2.0
134    */
135   private Boolean execute;
136
137   /**
138    * Skip execution
139    * <p>
140    * If set to <code>true</code>, the execution is skipped.
141    * <p>
142    * A skipped execution is signaled via the maven-property
143    * <code>${hibernate.schema.skipped}</code>.
144    * <p>
145    * The execution is skipped automatically, if no modified or newly added
146    * annotated classes are found and the dialect was not changed.
147    * <p>
148    * <strong>Important:</strong>
149    * This configuration value can only be configured through the
150    * <code>pom.xml</code>, or by the definition of a system-property, because
151    * it is not known by Hibernate nor JPA and, hence, not picked up from
152    * their configuration!
153    *
154    * @parameter property="hibernate.schema.skip" default-value="${maven.test.skip}"
155    * @since 1.0
156    */
157   private boolean skip;
158
159   /**
160    * Force generation/execution
161    * <p>
162    * Force the generation and (if configured) the execution of the SQL, even if
163    * no modified or newly added annotated classes where found and the
164    * configuration was not changed.
165    * <p>
166    * <code>skip</code> takes precedence over <code>force</code>.
167    * <p>
168    * <strong>Important:</strong>
169    * This configuration value can only be configured through the
170    * <code>pom.xml</code>, or by the definition of a system-property, because
171    * it is not known by Hibernate nor JPA and, hence, not picked up from
172    * their configuration!
173    *
174    * @parameter property="hibernate.schema.force" default-value="false"
175    * @since 1.0
176    */
177   private boolean force;
178
179   /**
180    * Hibernate dialect.
181    *
182    * @parameter property="hibernate.dialect"
183    * @since 1.0
184    */
185   private String dialect;
186
187   /**
188    * Delimiter in output-file.
189    * <p>
190    * <strong>Important:</strong>
191    * This configuration value can only be configured through the
192    * <code>pom.xml</code>, or by the definition of a system-property, because
193    * it is not known by Hibernate nor JPA and, hence, not picked up from
194    * their configuration!
195    *
196    * @parameter property="hibernate.hbm2ddl.delimiter" default-value=";"
197    * @since 1.0
198    */
199   private String delimiter;
200
201   /**
202    * Show the generated SQL in the command-line output.
203    *
204    * @parameter property="hibernate.show_sql"
205    * @since 1.0
206    */
207   private Boolean show;
208
209   /**
210    * Format output-file.
211    *
212    * @parameter property="hibernate.format_sql"
213    * @since 1.0
214    */
215   private Boolean format;
216
217   /**
218    * Specifies whether to automatically create also the database schema/catalog.
219    *
220    * @parameter property="hibernate.hbm2dll.create_namespaces" default-value="false"
221    * @since 2.0
222    */
223   private Boolean createNamespaces;
224
225   /**
226    * Implicit naming strategy
227    *
228    * @parameter property="hibernate.implicit_naming_strategy"
229    * @since 2.0
230    */
231   private String implicitNamingStrategy;
232
233   /**
234    * Physical naming strategy
235    *
236    * @parameter property="hibernate.physical_naming_strategy"
237    * @since 2.0
238    */
239   private String physicalNamingStrategy;
240
241   /**
242    * Wether the project should be scanned for annotated-classes, or not
243    * <p>
244    * This parameter is intended to allow overwriting of the parameter
245    * <code>exclude-unlisted-classes</code> of a <code>persistence-unit</code>.
246    * If not specified, it defaults to <code>true</code>
247    *
248    * @parameter property="hibernate.schema.scan.classes"
249    * @since 2.0
250    */
251   private Boolean scanClasses;
252
253   /**
254    * Classes-Directory to scan.
255    * <p>
256    * This parameter defaults to the maven build-output-directory for classes.
257    * Additionally, all dependencies are scanned for annotated classes.
258    * <p>
259    * <strong>Important:</strong>
260    * This configuration value can only be configured through the
261    * <code>pom.xml</code>, or by the definition of a system-property, because
262    * it is not known by Hibernate nor JPA and, hence, not picked up from
263    * their configuration!
264    *
265    * @parameter property="project.build.outputDirectory"
266    * @since 1.0
267    */
268   private String outputDirectory;
269
270   /**
271    * Dependency-Scopes, that should be scanned for annotated classes.
272    * <p>
273    * By default, only dependencies in the scope <code>compile</code> are
274    * scanned for annotated classes. Multiple scopes can be seperated by
275    * white space or commas.
276    * <p>
277    * If you do not want any dependencies to be scanned for annotated
278    * classes, set this parameter to <code>none</code>.
279    * <p>
280    * The plugin does not scan for annotated classes in transitive
281    * dependencies. If some of your annotated classes are hidden in a
282    * transitive dependency, you can simply add that dependency explicitly.
283    *
284    * @parameter property="hibernate.schema.scan.dependencies" default-value="compile"
285    * @since 1.0.3
286    */
287   private String scanDependencies;
288
289   /**
290    * Whether to scan the test-branch of the project for annotated classes, or
291    * not.
292    * <p>
293    * If this parameter is set to <code>true</code> the test-classes of the
294    * artifact will be scanned for hibernate-annotated classes additionally.
295    * <p>
296    * <strong>Important:</strong>
297    * This configuration value can only be configured through the
298    * <code>pom.xml</code>, or by the definition of a system-property, because
299    * it is not known by Hibernate nor JPA and, hence, not picked up from
300    * their configuration!
301    *
302    * @parameter property="hibernate.schema.scan.test_classes" default-value="false"
303    * @since 1.0.1
304    */
305   private Boolean scanTestClasses;
306
307   /**
308    * Test-Classes-Directory to scan.
309    * <p>
310    * This parameter defaults to the maven build-output-directory for
311    * test-classes.
312    * <p>
313    * This parameter is only used, when <code>scanTestClasses</code> is set
314    * to <code>true</code>!
315    * <p>
316    * <strong>Important:</strong>
317    * This configuration value can only be configured through the
318    * <code>pom.xml</code>, or by the definition of a system-property, because
319    * it is not known by Hibernate nor JPA and, hence, not picked up from
320    * their configuration!
321    *
322    * @parameter property="project.build.testOutputDirectory"
323    * @since 1.0.2
324    */
325   private String testOutputDirectory;
326
327
328   /** Conection parameters *************************************************/
329
330   /**
331    * SQL-Driver name.
332    *
333    * @parameter property="hibernate.connection.driver_class"
334    * @since 1.0
335    */
336   private String driver;
337
338   /**
339    * Database URL.
340    *
341    * @parameter property="hibernate.connection.url"
342    * @since 1.0
343    */
344   private String url;
345
346   /**
347    * Database username
348    *
349    * @parameter property="hibernate.connection.username"
350    * @since 1.0
351    */
352   private String username;
353
354   /**
355    * Database password
356    *
357    * @parameter property="hibernate.connection.password"
358    * @since 1.0
359    */
360   private String password;
361
362
363   /** Parameters to locate configuration sources ****************************/
364
365   /**
366    * Path to a file or name of a ressource with hibernate properties.
367    * If this parameter is specified, the plugin will try to load configuration
368    * values from a file with the given path or a ressource on the classpath with
369    * the given name. If both fails, the execution of the plugin will fail.
370    * <p>
371    * If this parameter is not set the plugin will load configuration values
372    * from a ressource named <code>hibernate.properties</code> on the classpath,
373    * if it is present, but will not fail if there is no such ressource.
374    * <p>
375    * During ressource-lookup, the test-classpath takes precedence.
376    *
377    * @parameter
378    * @since 1.0
379    */
380   private String hibernateProperties;
381
382   /**
383    * Path to Hibernate configuration file (.cfg.xml).
384    * If this parameter is specified, the plugin will try to load configuration
385    * values from a file with the given path or a ressource on the classpath with
386    * the given name. If both fails, the execution of the plugin will fail.
387    * <p>
388    * If this parameter is not set the plugin will load configuration values
389    * from a ressource named <code>hibernate.cfg.xml</code> on the classpath,
390    * if it is present, but will not fail if there is no such ressource.
391    * <p>
392    * During ressource-lookup, the test-classpath takes precedence.
393    * <p>
394    * Settings in this file will overwrite settings in the properties file.
395    *
396    * @parameter
397    * @since 1.1.0
398    */
399   private String hibernateConfig;
400
401   /**
402    * Name of the persistence-unit.
403    * If this parameter is specified, the plugin will try to load configuration
404    * values from a persistence-unit with the specified name. If no such
405    * persistence-unit can be found, the plugin will throw an exception.
406    * <p>
407    * If this parameter is not set and there is only one persistence-unit
408    * available, that unit will be used automatically. But if this parameter is
409    * not set and there are multiple persistence-units available on,
410    * the class-path, the execution of the plugin will fail.
411    * <p>
412    * Settings in this file will overwrite settings in the properties or the
413    * configuration file.
414    *
415    * @parameter
416    * @since 1.1.0
417    */
418   private String persistenceUnit;
419
420   /**
421    * List of Hibernate-Mapping-Files (XML).
422    * Multiple files can be separated with white-spaces and/or commas.
423    *
424    * @parameter property="hibernate.mapping"
425    * @since 1.0.2
426    */
427   private String mappings;
428
429
430
431   public final void execute(String filename)
432     throws
433       MojoFailureException,
434       MojoExecutionException
435   {
436     if (skip)
437     {
438       getLog().info("Execution of hibernate-maven-plugin was skipped!");
439       project.getProperties().setProperty(SKIPPED, "true");
440       return;
441     }
442
443     ModificationTracker tracker;
444     try
445     {
446       tracker = new ModificationTracker(buildDirectory, filename, getLog());
447     }
448     catch (NoSuchAlgorithmException e)
449     {
450       throw new MojoFailureException("Digest-Algorithm MD5 is missing!", e);
451     }
452
453     final SimpleConnectionProvider connectionProvider =
454         new SimpleConnectionProvider(getLog());
455
456     try
457     {
458       /** Start extended logging */
459       MavenLogAppender.startPluginLog(this);
460
461       /** Load checksums for old mapping and configuration */
462       tracker.load();
463
464       /** Create the ClassLoader */
465       MutableClassLoader classLoader = createClassLoader();
466
467       /** Create a BootstrapServiceRegistry with the created ClassLoader */
468       BootstrapServiceRegistry bootstrapServiceRegitry =
469           new BootstrapServiceRegistryBuilder()
470               .applyClassLoader(classLoader)
471               .build();
472       ClassLoaderService classLoaderService =
473           bootstrapServiceRegitry.getService(ClassLoaderService.class);
474
475       Properties properties = new Properties();
476       ConfigLoader configLoader = new ConfigLoader(bootstrapServiceRegitry);
477
478       /** Loading and merging configuration */
479       properties.putAll(loadProperties(configLoader));
480       LoadedConfig config = loadConfig(configLoader);
481       if (config != null)
482         properties.putAll(config.getConfigurationValues());
483       ParsedPersistenceXmlDescriptor unit =
484           loadPersistenceUnit(classLoaderService, properties);
485       if (unit != null)
486         properties.putAll(unit.getProperties());
487
488       /** Overwriting/Completing configuration */
489       configure(properties, tracker);
490
491       /** Check configuration for modifications */
492       if(tracker.track(properties))
493         getLog().debug("Configuration has changed.");
494       else
495         getLog().debug("Configuration unchanged.");
496
497       /** Check, that the outputfile is writable */
498       final File output = getOutputFile(filename);
499       /** Check, if the outputfile is missing or was changed */
500       checkOutputFile(output, tracker);
501
502       /** Configure Hibernate */
503       final StandardServiceRegistry serviceRegistry =
504           new StandardServiceRegistryBuilder(bootstrapServiceRegitry)
505               .applySettings(properties)
506               .addService(ConnectionProvider.class, connectionProvider)
507               .build();
508       final MetadataSources sources = new MetadataSources(serviceRegistry);
509
510       /** Add the remaining class-path-elements */
511       completeClassPath(classLoader);
512
513       /** Apply mappings from hibernate-configuration, if present */
514       if (config != null)
515       {
516         for (MappingReference mapping : config.getMappingReferences())
517           mapping.apply(sources);
518       }
519
520       Set<String> classes;
521       if (unit == null)
522       {
523         /** No persistent unit: default behaviour */
524         if (scanClasses == null)
525           scanClasses = true;
526         Set<URL> urls = new HashSet<URL>();
527         if (scanClasses)
528           addRoot(urls, outputDirectory);
529         if (scanTestClasses)
530           addRoot(urls, testOutputDirectory);
531         addDependencies(urls);
532         classes = scanUrls(urls);
533       }
534       else
535       {
536         /** Follow configuration in persisten unit */
537         if (scanClasses == null)
538           scanClasses = !unit.isExcludeUnlistedClasses();
539         Set<URL> urls = new HashSet<URL>();
540         if (scanClasses)
541         {
542           /**
543            * Scan the root of the persiten unit and configured jars for
544            * annotated classes
545            */
546           urls.add(unit.getPersistenceUnitRootUrl());
547           for (URL url : unit.getJarFileUrls())
548             urls.add(url);
549         }
550         if (scanTestClasses)
551           addRoot(urls, testOutputDirectory);
552         classes = scanUrls(urls);
553         for (String className : unit.getManagedClassNames())
554           classes.add(className);
555         /**
556          * Add mappings from the default mapping-file
557          * <code>META-INF/orm.xml</code>, if present
558          */
559         boolean error = false;
560         InputStream is;
561         is = classLoader.getResourceAsStream("META-INF/orm.xml");
562         if (is != null)
563         {
564           getLog().info("Adding default JPA-XML-mapping from META-INF/orm.xml");
565           try
566           {
567             tracker.track("META-INF/orm.xml", is);
568             sources.addResource("META-INF/orm.xml");
569           }
570           catch (IOException e)
571           {
572             getLog().error("cannot read META-INF/orm.xml: " + e);
573             error = true;
574           }
575         }
576         else
577         {
578           getLog().debug("no META-INF/orm.xml found");
579         }
580         /**
581          * Add mappings from files, that are explicitly configured in the
582          * persistence unit
583          */
584         for (String mapping : unit.getMappingFileNames())
585         {
586           getLog().info("Adding explicitly configured mapping from " + mapping);
587           is = classLoader.getResourceAsStream(mapping);
588           if (is != null)
589           {
590             try
591             {
592               tracker.track(mapping, is);
593               sources.addResource(mapping);
594             }
595             catch (IOException e)
596             {
597               getLog().info("cannot read mapping-file " + mapping + ": " + e);
598               error = true;
599             }
600           }
601           else
602           {
603             getLog().error("cannot find mapping-file " + mapping);
604             error = true;
605           }
606         }
607         if (error)
608           throw new MojoFailureException(
609               "error, while reading mappings configured in persistence-unit \"" +
610               unit.getName() +
611               "\""
612               );
613       }
614
615       /** Add the configured/collected annotated classes */
616       for (String className : classes)
617         addAnnotated(className, sources, classLoaderService, tracker);
618
619       /** Add explicitly configured classes */
620       addMappings(sources, tracker);
621
622       /** Skip execution, if mapping and configuration is unchanged */
623       if (!tracker.modified())
624       {
625         getLog().info("Mapping and configuration unchanged.");
626         if (force)
627           getLog().info("Generation/execution is forced!");
628         else
629         {
630           getLog().info("Skipping schema generation!");
631           project.getProperties().setProperty(SKIPPED, "true");
632           return;
633         }
634       }
635
636
637       /** Truncate output file */
638       try
639       {
640         new FileOutputStream(output).getChannel().truncate(0).close();
641       }
642       catch (IOException e)
643       {
644         String error =
645             "Error while truncating " + output.getAbsolutePath() + ": "
646             + e.getMessage();
647         getLog().warn(error);
648         throw new MojoExecutionException(error);
649       }
650
651       /** Create a connection, if sufficient configuration infromation is available */
652       connectionProvider.open(classLoaderService, properties);
653
654       MetadataBuilder metadataBuilder = sources.getMetadataBuilder();
655
656       StrategySelector strategySelector =
657           serviceRegistry.getService(StrategySelector.class);
658
659       if (properties.containsKey(IMPLICIT_NAMING_STRATEGY))
660       {
661         metadataBuilder.applyImplicitNamingStrategy(
662             strategySelector.resolveStrategy(
663                 ImplicitNamingStrategy.class,
664                 properties.getProperty(IMPLICIT_NAMING_STRATEGY)
665                 )
666             );
667       }
668
669       if (properties.containsKey(PHYSICAL_NAMING_STRATEGY))
670       {
671         metadataBuilder.applyPhysicalNamingStrategy(
672             strategySelector.resolveStrategy(
673                 PhysicalNamingStrategy.class,
674                 properties.getProperty(PHYSICAL_NAMING_STRATEGY)
675                 )
676             );
677       }
678
679       /**
680        * Change class-loader of current thread.
681        * This is necessary, because still not all parts of Hibernate 5 use
682        * the newly introduced ClassLoaderService and will fail otherwise!
683        */
684       Thread thread = Thread.currentThread();
685       ClassLoader contextClassLoader = thread.getContextClassLoader();
686       try
687       {
688         thread.setContextClassLoader(classLoader);
689         ExceptionHandlerCollectingImpl handler =
690             build((MetadataImplementor)metadataBuilder.build());
691         if (handler.getExceptions().size() > 0)
692         {
693           StringBuilder builder = new StringBuilder();
694           builder.append("Hibernate failed:");
695           for (Exception e : handler.getExceptions())
696           {
697             builder.append("\n * ");
698             builder.append(e.getMessage());
699           }
700           String error = builder.toString();
701           getLog().error(error);
702           throw new MojoFailureException(error);
703         }
704       }
705       finally
706       {
707         thread.setContextClassLoader(contextClassLoader);
708         /** Track, the content of the generated script */
709         checkOutputFile(output, tracker);
710       }
711     }
712     catch (MojoExecutionException e)
713     {
714       tracker.failed();
715       throw e;
716     }
717     catch (MojoFailureException e)
718     {
719       tracker.failed();
720       throw e;
721     }
722     catch (RuntimeException e)
723     {
724       tracker.failed();
725       throw e;
726     }
727     finally
728     {
729       /** Remember mappings and configuration */
730       tracker.save();
731
732       /** Close the connection - if one was opened */
733       connectionProvider.close();
734
735       /** Stop Log-Capturing */
736       MavenLogAppender.endPluginLog(this);
737     }
738   }
739
740
741   abstract ExceptionHandlerCollectingImpl build(MetadataImplementor metadata)
742     throws
743       MojoFailureException,
744       MojoExecutionException;
745
746
747   private MutableClassLoader createClassLoader() throws MojoExecutionException
748   {
749     try
750     {
751       getLog().debug("Creating ClassLoader for project-dependencies...");
752       LinkedHashSet<URL> urls = new LinkedHashSet<URL>();
753       File file;
754
755       file = new File(testOutputDirectory);
756       if (!file.exists())
757       {
758         getLog().info("Creating test-output-directory: " + testOutputDirectory);
759         file.mkdirs();
760       }
761       urls.add(file.toURI().toURL());
762
763       file = new File(outputDirectory);
764       if (!file.exists())
765       {
766         getLog().info("Creating output-directory: " + outputDirectory);
767         file.mkdirs();
768       }
769       urls.add(file.toURI().toURL());
770
771       return new MutableClassLoader(urls, getLog());
772     }
773     catch (Exception e)
774     {
775       getLog().error("Error while creating ClassLoader!", e);
776       throw new MojoExecutionException(e.getMessage());
777     }
778   }
779
780   private void completeClassPath(MutableClassLoader classLoader)
781       throws
782         MojoExecutionException
783   {
784     try
785     {
786       getLog().debug("Completing class-paths of the ClassLoader for project-dependencies...");
787       List<String> classpathFiles = project.getCompileClasspathElements();
788       if (scanTestClasses)
789         classpathFiles.addAll(project.getTestClasspathElements());
790       LinkedHashSet<URL> urls = new LinkedHashSet<URL>();
791       for (String pathElement : classpathFiles)
792       {
793         getLog().debug("Dependency: " + pathElement);
794         urls.add(new File(pathElement).toURI().toURL());
795       }
796       classLoader.add(urls);
797     }
798     catch (Exception e)
799     {
800       getLog().error("Error while creating ClassLoader!", e);
801       throw new MojoExecutionException(e.getMessage());
802     }
803   }
804
805   private Map loadProperties(ConfigLoader configLoader)
806       throws
807         MojoExecutionException
808   {
809     /** Try to read configuration from properties-file */
810     if (hibernateProperties == null)
811     {
812       try
813       {
814         return configLoader.loadProperties("hibernate.properties");
815       }
816       catch (ConfigurationException e)
817       {
818         getLog().debug(e.getMessage());
819         return Collections.EMPTY_MAP;
820       }
821     }
822     else
823     {
824       try
825       {
826         File file = new File(hibernateProperties);
827         if (file.exists())
828         {
829           getLog().info("Reading settings from file " + hibernateProperties + "...");
830           return configLoader.loadProperties(file);
831         }
832         else
833           return configLoader.loadProperties(hibernateProperties);
834       }
835       catch (ConfigurationException e)
836       {
837         getLog().error("Error while reading properties!", e);
838         throw new MojoExecutionException(e.getMessage());
839       }
840     }
841   }
842
843   private LoadedConfig loadConfig(ConfigLoader configLoader)
844       throws MojoExecutionException
845   {
846     /** Try to read configuration from configuration-file */
847     if (hibernateConfig == null)
848     {
849       try
850       {
851         return configLoader.loadConfigXmlResource("hibernate.cfg.xml");
852       }
853       catch (ConfigurationException e)
854       {
855         getLog().debug(e.getMessage());
856         return null;
857       }
858     }
859     else
860     {
861       try
862       {
863         File file = new File(hibernateConfig);
864         if (file.exists())
865         {
866           getLog().info("Reading configuration from file " + hibernateConfig + "...");
867           return configLoader.loadConfigXmlFile(file);
868         }
869         else
870         {
871           return configLoader.loadConfigXmlResource(hibernateConfig);
872         }
873       }
874       catch (ConfigurationException e)
875       {
876         getLog().error("Error while reading configuration!", e);
877         throw new MojoExecutionException(e.getMessage());
878       }
879     }
880   }
881
882   private void configure(Properties properties, ModificationTracker tracker)
883       throws MojoFailureException
884   {
885     /**
886      * Special treatment for the configuration-value "execute": if it is
887      * switched to "true", the genearation fo the schema should be forced!
888      */
889     if (tracker.check(EXECUTE, execute.toString()) && execute)
890     {
891       getLog().info(
892           "hibernate.schema.execute was switched on: " +
893           "forcing generation/execution of SQL"
894           );
895       tracker.touch();
896     }
897     configure(properties, execute, EXECUTE);
898
899     /**
900      * Configure the generation of the SQL.
901      * Overwrite values from properties-file if the configuration parameter is
902      * known to Hibernate.
903      */
904     configure(properties, dialect, DIALECT);
905     configure(properties, delimiter, HBM2DDL_DELIMITER);
906     configure(properties, format, FORMAT_SQL);
907     configure(properties, createNamespaces, HBM2DLL_CREATE_NAMESPACES);
908     configure(properties, implicitNamingStrategy, IMPLICIT_NAMING_STRATEGY);
909     configure(properties, physicalNamingStrategy, PHYSICAL_NAMING_STRATEGY);
910     configure(properties, outputDirectory, OUTPUTDIRECTORY);
911     configure(properties, scanDependencies, SCAN_DEPENDENCIES);
912     configure(properties, scanTestClasses, SCAN_TESTCLASSES);
913     configure(properties, testOutputDirectory, TEST_OUTPUTDIRECTORY);
914
915     /**
916      * Special treatment for the configuration-value "show": a change of its
917      * configured value should not lead to a regeneration of the database
918      * schama!
919      */
920     if (show == null)
921       show = Boolean.valueOf(properties.getProperty(SHOW_SQL));
922     else
923       properties.setProperty(SHOW_SQL, show.toString());
924
925     /**
926      * Configure the connection parameters.
927      * Overwrite values from properties-file.
928      */
929     configure(properties, driver, DRIVER, JPA_JDBC_DRIVER);
930     configure(properties, url, URL, JPA_JDBC_URL);
931     configure(properties, username, USER, JPA_JDBC_USER);
932     configure(properties, password, PASS, JPA_JDBC_PASSWORD);
933
934     if (properties.isEmpty())
935     {
936       getLog().error("No properties set!");
937       throw new MojoFailureException("Hibernate configuration is missing!");
938     }
939
940     getLog().info("Gathered configuration:");
941     for (Entry<Object,Object> entry : properties.entrySet())
942       getLog().info("  " + entry.getKey() + " = " + entry.getValue());
943   }
944
945   private void configure(
946       Properties properties,
947       String value,
948       String key,
949       String alternativeKey
950       )
951   {
952     configure(properties, value, key);
953
954     if (properties.containsKey(alternativeKey))
955     {
956       if (properties.containsKey(key))
957       {
958         getLog().warn(
959             "Ignoring property " + alternativeKey + "=\"" +
960             properties.getProperty(alternativeKey) +
961             "\" in favour for property " + key + "=\"" +
962             properties.getProperty(key) + "\""
963             );
964         properties.remove(alternativeKey);
965       }
966       else
967       {
968         value = properties.getProperty(alternativeKey);
969         properties.remove(alternativeKey);
970         getLog().info(
971             "Using value \"" + value + "\" from property " + alternativeKey +
972             " for property " + key
973             );
974         properties.setProperty(key, value);
975       }
976     }
977   }
978
979   private void configure(Properties properties, String value, String key)
980   {
981     if (value != null)
982     {
983       if (properties.containsKey(key))
984       {
985         if (!properties.getProperty(key).equals(value))
986         {
987           getLog().info(
988               "Overwriting property " + key + "=\"" +
989               properties.getProperty(key) +
990               "\" with value \"" + value + "\""
991               );
992           properties.setProperty(key, value);
993         }
994       }
995       else
996       {
997         getLog().debug("Using value \"" + value + "\" for property " + key);
998         properties.setProperty(key, value);
999       }
1000     }
1001   }
1002
1003   private void configure(Properties properties, Boolean value, String key)
1004   {
1005     configure(properties, value == null ? null : value.toString(), key);
1006   }
1007
1008   private File getOutputFile(String filename)
1009       throws
1010         MojoExecutionException
1011   {
1012     File output = new File(filename);
1013
1014     if (!output.isAbsolute())
1015     {
1016       // Interpret relative file path relative to build directory
1017       output = new File(buildDirectory, filename);
1018     }
1019     getLog().debug("Output file: " + output.getPath());
1020
1021     // Ensure that directory path for specified file exists
1022     File outFileParentDir = output.getParentFile();
1023     if (null != outFileParentDir && !outFileParentDir.exists())
1024     {
1025       try
1026       {
1027         getLog().info(
1028             "Creating directory path for output file:" +
1029             outFileParentDir.getPath()
1030             );
1031         outFileParentDir.mkdirs();
1032       }
1033       catch (Exception e)
1034       {
1035         String error =
1036             "Error creating directory path for output file: " + e.getMessage();
1037         getLog().error(error);
1038         throw new MojoExecutionException(error);
1039       }
1040     }
1041
1042     try
1043     {
1044       output.createNewFile();
1045     }
1046     catch (IOException e)
1047     {
1048       String error = "Error creating output file: " + e.getMessage();
1049       getLog().error(error);
1050       throw new MojoExecutionException(error);
1051     }
1052
1053     if (!output.canWrite())
1054     {
1055       String error =
1056           "Output file " + output.getAbsolutePath() + " is not writable!";
1057       getLog().error(error);
1058       throw new MojoExecutionException(error);
1059     }
1060
1061     return output;
1062   }
1063
1064   private void checkOutputFile(File output, ModificationTracker tracker)
1065       throws
1066         MojoExecutionException
1067   {
1068     try
1069     {
1070       if (output.exists())
1071         tracker.track(SCRIPT, new FileInputStream(output));
1072       else
1073         tracker.track(SCRIPT, ZonedDateTime.now().toString());
1074     }
1075     catch (IOException e)
1076     {
1077       String error =
1078           "Error while checking the generated script: " + e.getMessage();
1079       getLog().error(error);
1080       throw new MojoExecutionException(error);
1081     }
1082   }
1083
1084   private void addMappings(MetadataSources sources, ModificationTracker tracker)
1085       throws MojoFailureException
1086   {
1087     getLog().debug("Adding explicitly configured mappings...");
1088     if (mappings != null)
1089     {
1090       try
1091       {
1092         for (String filename : mappings.split("[\\s,]+"))
1093         {
1094           // First try the filename as absolute/relative path
1095           File file = new File(filename);
1096           if (!file.exists())
1097           {
1098             // If the file was not found, search for it in the resource-directories
1099             for (Resource resource : project.getResources())
1100             {
1101               file = new File(resource.getDirectory() + File.separator + filename);
1102               if (file.exists())
1103                 break;
1104             }
1105           }
1106           if (file.exists())
1107           {
1108             if (file.isDirectory())
1109               // TODO: add support to read all mappings under a directory
1110               throw new MojoFailureException(file.getAbsolutePath() + " is a directory");
1111             if (tracker.track(filename, new FileInputStream(file)))
1112               getLog().debug("Found new or modified mapping-file: " + filename);
1113             else
1114               getLog().debug("Mapping-file unchanged: " + filename);
1115
1116             sources.addFile(file);
1117           }
1118           else
1119             throw new MojoFailureException("File " + filename + " could not be found in any of the configured resource-directories!");
1120         }
1121       }
1122       catch (IOException e)
1123       {
1124         throw new MojoFailureException("Cannot calculate MD5 sums!", e);
1125       }
1126     }
1127   }
1128
1129   private void addRoot(Set<URL> urls, String path) throws MojoFailureException
1130   {
1131     try
1132     {
1133       File dir = new File(outputDirectory);
1134       if (dir.exists())
1135       {
1136         getLog().info("Adding " + dir.getAbsolutePath() + " to the list of roots to scan...");
1137         urls.add(dir.toURI().toURL());
1138       }
1139     }
1140     catch (MalformedURLException e)
1141     {
1142       getLog().error("error while adding the project-root to the list of roots to scan!", e);
1143       throw new MojoFailureException(e.getMessage());
1144     }
1145   }
1146
1147   private void addDependencies(Set<URL> urls) throws MojoFailureException
1148   {
1149     try
1150     {
1151       if (scanDependencies != null)
1152       {
1153         Matcher matcher = SPLIT.matcher(scanDependencies);
1154         while (matcher.find())
1155         {
1156           getLog().info("Adding dependencies from scope " + matcher.group() + " to the list of roots to scan");
1157           for (Artifact artifact : project.getDependencyArtifacts())
1158           {
1159             if (!artifact.getScope().equalsIgnoreCase(matcher.group()))
1160               continue;
1161             if (artifact.getFile() == null)
1162             {
1163               getLog().warn("Cannot add dependency " + artifact.getId() + ": no JAR-file available!");
1164               continue;
1165             }
1166             getLog().info("Adding dependencies from scope " + artifact.getId() + " to the list of roots to scan");
1167             urls.add(artifact.getFile().toURI().toURL());
1168           }
1169         }
1170       }
1171     }
1172     catch (MalformedURLException e)
1173     {
1174       getLog().error("Error while adding dependencies to the list of roots to scan!", e);
1175       throw new MojoFailureException(e.getMessage());
1176     }
1177   }
1178
1179   private Set<String> scanUrls(Set<URL> scanRoots)
1180       throws
1181         MojoFailureException
1182   {
1183     try
1184     {
1185       AnnotationDB db = new AnnotationDB();
1186       for (URL root : scanRoots)
1187         db.scanArchives(root);
1188
1189       Set<String> classes = new HashSet<String>();
1190       if (db.getAnnotationIndex().containsKey(Entity.class.getName()))
1191         classes.addAll(db.getAnnotationIndex().get(Entity.class.getName()));
1192       if (db.getAnnotationIndex().containsKey(MappedSuperclass.class.getName()))
1193         classes.addAll(db.getAnnotationIndex().get(MappedSuperclass.class.getName()));
1194       if (db.getAnnotationIndex().containsKey(Embeddable.class.getName()))
1195         classes.addAll(db.getAnnotationIndex().get(Embeddable.class.getName()));
1196
1197       return classes;
1198     }
1199     catch (Exception e)
1200     {
1201       getLog().error("Error while scanning!", e);
1202       throw new MojoFailureException(e.getMessage());
1203     }
1204   }
1205
1206   private void addAnnotated(
1207       String name,
1208       MetadataSources sources,
1209       ClassLoaderService classLoaderService,
1210       ModificationTracker tracker
1211       )
1212       throws
1213         MojoFailureException,
1214         MojoExecutionException
1215   {
1216     try
1217     {
1218       getLog().info("Adding annotated resource: " + name);
1219       String packageName = null;
1220
1221       boolean error = false;
1222       try
1223       {
1224         Class<?> annotatedClass = classLoaderService.classForName(name);
1225         String resourceName = annotatedClass.getName();
1226         resourceName =
1227             resourceName.substring(
1228                 resourceName.lastIndexOf(".") + 1,
1229                 resourceName.length()
1230                 ) + ".class";
1231         InputStream is = annotatedClass.getResourceAsStream(resourceName);
1232         if (is != null)
1233         {
1234           if (tracker.track(name, is))
1235             getLog().debug("New or modified class: " + name);
1236           else
1237             getLog().debug("Unchanged class: " + name);
1238           sources.addAnnotatedClass(annotatedClass);
1239           packageName = annotatedClass.getPackage().getName();
1240         }
1241         else
1242         {
1243           getLog().error("cannot find ressource " + resourceName + " for class " + name);
1244           error = true;
1245         }
1246       }
1247       catch(ClassLoadingException e)
1248       {
1249         packageName = name;
1250       }
1251       if (error)
1252       {
1253         throw new MojoExecutionException("error while inspecting annotated class " + name);
1254       }
1255
1256       while (packageName != null)
1257       {
1258         if (packages.contains(packageName))
1259           return;
1260         String resource = packageName.replace('.', '/') + "/package-info.class";
1261         InputStream is = classLoaderService.locateResourceStream(resource);
1262         if (is == null)
1263         {
1264           // No compiled package-info available: no package-level annotations!
1265           getLog().debug("Package " + packageName + " is not annotated.");
1266         }
1267         else
1268         {
1269           if (tracker.track(packageName, is))
1270             getLog().debug("New or modified package: " + packageName);
1271           else
1272            getLog().debug("Unchanged package: " + packageName);
1273           getLog().info("Adding annotations from package " + packageName);
1274           sources.addPackage(packageName);
1275         }
1276         packages.add(packageName);
1277         int i = packageName.lastIndexOf('.');
1278         if (i < 0)
1279           packageName = null;
1280         else
1281           packageName = packageName.substring(0,i);
1282       }
1283     }
1284     catch (Exception e)
1285     {
1286       getLog().error("Error while adding the annotated class " + name, e);
1287       throw new MojoFailureException(e.getMessage());
1288     }
1289   }
1290
1291   private ParsedPersistenceXmlDescriptor loadPersistenceUnit(
1292       ClassLoaderService classLoaderService,
1293       Properties properties
1294       )
1295       throws
1296         MojoFailureException
1297   {
1298     PersistenceXmlParser parser =
1299         new PersistenceXmlParser(
1300             classLoaderService,
1301             PersistenceUnitTransactionType.RESOURCE_LOCAL
1302              );
1303
1304     Map<String, ParsedPersistenceXmlDescriptor> units =
1305         parser.doResolve(properties);
1306
1307     if (persistenceUnit == null)
1308     {
1309       Iterator<String> names = units.keySet().iterator();
1310       if (!names.hasNext())
1311       {
1312         getLog().info("Found no META-INF/persistence.xml.");
1313         return null;
1314       }
1315
1316       String name = names.next();
1317       if (!names.hasNext())
1318       {
1319           getLog().info("Using persistence-unit " + name);
1320           return units.get(name);
1321       }
1322
1323       StringBuilder builder = new StringBuilder();
1324       builder.append("No name provided and multiple persistence units found: ");
1325       builder.append(name);
1326       while(names.hasNext())
1327       {
1328         builder.append(", ");
1329         builder.append(names.next());
1330       }
1331       builder.append('.');
1332       throw new MojoFailureException(builder.toString());
1333     }
1334
1335     if (units.containsKey(persistenceUnit))
1336     {
1337       getLog().info("Using configured persistence-unit " + persistenceUnit);
1338       return units.get(persistenceUnit);
1339     }
1340
1341     throw new MojoFailureException("Could not find persistence-unit " + persistenceUnit);
1342   }
1343 }