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(), output);
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(
742       MetadataImplementor metadata,
743       File file
744       )
745     throws
746       MojoFailureException,
747       MojoExecutionException;
748
749
750   private MutableClassLoader createClassLoader() throws MojoExecutionException
751   {
752     try
753     {
754       getLog().debug("Creating ClassLoader for project-dependencies...");
755       LinkedHashSet<URL> urls = new LinkedHashSet<URL>();
756       File file;
757
758       file = new File(testOutputDirectory);
759       if (!file.exists())
760       {
761         getLog().info("Creating test-output-directory: " + testOutputDirectory);
762         file.mkdirs();
763       }
764       urls.add(file.toURI().toURL());
765
766       file = new File(outputDirectory);
767       if (!file.exists())
768       {
769         getLog().info("Creating output-directory: " + outputDirectory);
770         file.mkdirs();
771       }
772       urls.add(file.toURI().toURL());
773
774       return new MutableClassLoader(urls, getLog());
775     }
776     catch (Exception e)
777     {
778       getLog().error("Error while creating ClassLoader!", e);
779       throw new MojoExecutionException(e.getMessage());
780     }
781   }
782
783   private void completeClassPath(MutableClassLoader classLoader)
784       throws
785         MojoExecutionException
786   {
787     try
788     {
789       getLog().debug("Completing class-paths of the ClassLoader for project-dependencies...");
790       List<String> classpathFiles = project.getCompileClasspathElements();
791       if (scanTestClasses)
792         classpathFiles.addAll(project.getTestClasspathElements());
793       LinkedHashSet<URL> urls = new LinkedHashSet<URL>();
794       for (String pathElement : classpathFiles)
795       {
796         getLog().debug("Dependency: " + pathElement);
797         urls.add(new File(pathElement).toURI().toURL());
798       }
799       classLoader.add(urls);
800     }
801     catch (Exception e)
802     {
803       getLog().error("Error while creating ClassLoader!", e);
804       throw new MojoExecutionException(e.getMessage());
805     }
806   }
807
808   private Map loadProperties(ConfigLoader configLoader)
809       throws
810         MojoExecutionException
811   {
812     /** Try to read configuration from properties-file */
813     if (hibernateProperties == null)
814     {
815       try
816       {
817         return configLoader.loadProperties("hibernate.properties");
818       }
819       catch (ConfigurationException e)
820       {
821         getLog().debug(e.getMessage());
822         return Collections.EMPTY_MAP;
823       }
824     }
825     else
826     {
827       try
828       {
829         File file = new File(hibernateProperties);
830         if (file.exists())
831         {
832           getLog().info("Reading settings from file " + hibernateProperties + "...");
833           return configLoader.loadProperties(file);
834         }
835         else
836           return configLoader.loadProperties(hibernateProperties);
837       }
838       catch (ConfigurationException e)
839       {
840         getLog().error("Error while reading properties!", e);
841         throw new MojoExecutionException(e.getMessage());
842       }
843     }
844   }
845
846   private LoadedConfig loadConfig(ConfigLoader configLoader)
847       throws MojoExecutionException
848   {
849     /** Try to read configuration from configuration-file */
850     if (hibernateConfig == null)
851     {
852       try
853       {
854         return configLoader.loadConfigXmlResource("hibernate.cfg.xml");
855       }
856       catch (ConfigurationException e)
857       {
858         getLog().debug(e.getMessage());
859         return null;
860       }
861     }
862     else
863     {
864       try
865       {
866         File file = new File(hibernateConfig);
867         if (file.exists())
868         {
869           getLog().info("Reading configuration from file " + hibernateConfig + "...");
870           return configLoader.loadConfigXmlFile(file);
871         }
872         else
873         {
874           return configLoader.loadConfigXmlResource(hibernateConfig);
875         }
876       }
877       catch (ConfigurationException e)
878       {
879         getLog().error("Error while reading configuration!", e);
880         throw new MojoExecutionException(e.getMessage());
881       }
882     }
883   }
884
885   private void configure(Properties properties, ModificationTracker tracker)
886       throws MojoFailureException
887   {
888     /**
889      * Special treatment for the configuration-value "execute": if it is
890      * switched to "true", the genearation fo the schema should be forced!
891      */
892     if (tracker.check(EXECUTE, execute.toString()) && execute)
893     {
894       getLog().info(
895           "hibernate.schema.execute was switched on: " +
896           "forcing generation/execution of SQL"
897           );
898       tracker.touch();
899     }
900     configure(properties, execute, EXECUTE);
901
902     /**
903      * Configure the generation of the SQL.
904      * Overwrite values from properties-file if the configuration parameter is
905      * known to Hibernate.
906      */
907     configure(properties, dialect, DIALECT);
908     configure(properties, delimiter, HBM2DDL_DELIMITER);
909     configure(properties, format, FORMAT_SQL);
910     configure(properties, createNamespaces, HBM2DLL_CREATE_NAMESPACES);
911     configure(properties, implicitNamingStrategy, IMPLICIT_NAMING_STRATEGY);
912     configure(properties, physicalNamingStrategy, PHYSICAL_NAMING_STRATEGY);
913     configure(properties, outputDirectory, OUTPUTDIRECTORY);
914     configure(properties, scanDependencies, SCAN_DEPENDENCIES);
915     configure(properties, scanTestClasses, SCAN_TESTCLASSES);
916     configure(properties, testOutputDirectory, TEST_OUTPUTDIRECTORY);
917
918     /**
919      * Special treatment for the configuration-value "show": a change of its
920      * configured value should not lead to a regeneration of the database
921      * schama!
922      */
923     if (show == null)
924       show = Boolean.valueOf(properties.getProperty(SHOW_SQL));
925     else
926       properties.setProperty(SHOW_SQL, show.toString());
927
928     /**
929      * Configure the connection parameters.
930      * Overwrite values from properties-file.
931      */
932     configure(properties, driver, DRIVER, JPA_JDBC_DRIVER);
933     configure(properties, url, URL, JPA_JDBC_URL);
934     configure(properties, username, USER, JPA_JDBC_USER);
935     configure(properties, password, PASS, JPA_JDBC_PASSWORD);
936
937     if (properties.isEmpty())
938     {
939       getLog().error("No properties set!");
940       throw new MojoFailureException("Hibernate configuration is missing!");
941     }
942
943     getLog().info("Gathered configuration:");
944     for (Entry<Object,Object> entry : properties.entrySet())
945       getLog().info("  " + entry.getKey() + " = " + entry.getValue());
946   }
947
948   private void configure(
949       Properties properties,
950       String value,
951       String key,
952       String alternativeKey
953       )
954   {
955     configure(properties, value, key);
956
957     if (properties.containsKey(alternativeKey))
958     {
959       if (properties.containsKey(key))
960       {
961         getLog().warn(
962             "Ignoring property " + alternativeKey + "=\"" +
963             properties.getProperty(alternativeKey) +
964             "\" in favour for property " + key + "=\"" +
965             properties.getProperty(key) + "\""
966             );
967         properties.remove(alternativeKey);
968       }
969       else
970       {
971         value = properties.getProperty(alternativeKey);
972         properties.remove(alternativeKey);
973         getLog().info(
974             "Using value \"" + value + "\" from property " + alternativeKey +
975             " for property " + key
976             );
977         properties.setProperty(key, value);
978       }
979     }
980   }
981
982   private void configure(Properties properties, String value, String key)
983   {
984     if (value != null)
985     {
986       if (properties.containsKey(key))
987       {
988         if (!properties.getProperty(key).equals(value))
989         {
990           getLog().info(
991               "Overwriting property " + key + "=\"" +
992               properties.getProperty(key) +
993               "\" with value \"" + value + "\""
994               );
995           properties.setProperty(key, value);
996         }
997       }
998       else
999       {
1000         getLog().debug("Using value \"" + value + "\" for property " + key);
1001         properties.setProperty(key, value);
1002       }
1003     }
1004   }
1005
1006   private void configure(Properties properties, Boolean value, String key)
1007   {
1008     configure(properties, value == null ? null : value.toString(), key);
1009   }
1010
1011   private File getOutputFile(String filename)
1012       throws
1013         MojoExecutionException
1014   {
1015     File output = new File(filename);
1016
1017     if (!output.isAbsolute())
1018     {
1019       // Interpret relative file path relative to build directory
1020       output = new File(buildDirectory, filename);
1021     }
1022     getLog().debug("Output file: " + output.getPath());
1023
1024     // Ensure that directory path for specified file exists
1025     File outFileParentDir = output.getParentFile();
1026     if (null != outFileParentDir && !outFileParentDir.exists())
1027     {
1028       try
1029       {
1030         getLog().info(
1031             "Creating directory path for output file:" +
1032             outFileParentDir.getPath()
1033             );
1034         outFileParentDir.mkdirs();
1035       }
1036       catch (Exception e)
1037       {
1038         String error =
1039             "Error creating directory path for output file: " + e.getMessage();
1040         getLog().error(error);
1041         throw new MojoExecutionException(error);
1042       }
1043     }
1044
1045     try
1046     {
1047       output.createNewFile();
1048     }
1049     catch (IOException e)
1050     {
1051       String error = "Error creating output file: " + e.getMessage();
1052       getLog().error(error);
1053       throw new MojoExecutionException(error);
1054     }
1055
1056     if (!output.canWrite())
1057     {
1058       String error =
1059           "Output file " + output.getAbsolutePath() + " is not writable!";
1060       getLog().error(error);
1061       throw new MojoExecutionException(error);
1062     }
1063
1064     return output;
1065   }
1066
1067   private void checkOutputFile(File output, ModificationTracker tracker)
1068       throws
1069         MojoExecutionException
1070   {
1071     try
1072     {
1073       if (output.exists())
1074         tracker.track(SCRIPT, new FileInputStream(output));
1075       else
1076         tracker.track(SCRIPT, ZonedDateTime.now().toString());
1077     }
1078     catch (IOException e)
1079     {
1080       String error =
1081           "Error while checking the generated script: " + e.getMessage();
1082       getLog().error(error);
1083       throw new MojoExecutionException(error);
1084     }
1085   }
1086
1087   private void addMappings(MetadataSources sources, ModificationTracker tracker)
1088       throws MojoFailureException
1089   {
1090     getLog().debug("Adding explicitly configured mappings...");
1091     if (mappings != null)
1092     {
1093       try
1094       {
1095         for (String filename : mappings.split("[\\s,]+"))
1096         {
1097           // First try the filename as absolute/relative path
1098           File file = new File(filename);
1099           if (!file.exists())
1100           {
1101             // If the file was not found, search for it in the resource-directories
1102             for (Resource resource : project.getResources())
1103             {
1104               file = new File(resource.getDirectory() + File.separator + filename);
1105               if (file.exists())
1106                 break;
1107             }
1108           }
1109           if (file.exists())
1110           {
1111             if (file.isDirectory())
1112               // TODO: add support to read all mappings under a directory
1113               throw new MojoFailureException(file.getAbsolutePath() + " is a directory");
1114             if (tracker.track(filename, new FileInputStream(file)))
1115               getLog().debug("Found new or modified mapping-file: " + filename);
1116             else
1117               getLog().debug("Mapping-file unchanged: " + filename);
1118
1119             sources.addFile(file);
1120           }
1121           else
1122             throw new MojoFailureException("File " + filename + " could not be found in any of the configured resource-directories!");
1123         }
1124       }
1125       catch (IOException e)
1126       {
1127         throw new MojoFailureException("Cannot calculate MD5 sums!", e);
1128       }
1129     }
1130   }
1131
1132   private void addRoot(Set<URL> urls, String path) throws MojoFailureException
1133   {
1134     try
1135     {
1136       File dir = new File(outputDirectory);
1137       if (dir.exists())
1138       {
1139         getLog().info("Adding " + dir.getAbsolutePath() + " to the list of roots to scan...");
1140         urls.add(dir.toURI().toURL());
1141       }
1142     }
1143     catch (MalformedURLException e)
1144     {
1145       getLog().error("error while adding the project-root to the list of roots to scan!", e);
1146       throw new MojoFailureException(e.getMessage());
1147     }
1148   }
1149
1150   private void addDependencies(Set<URL> urls) throws MojoFailureException
1151   {
1152     try
1153     {
1154       if (scanDependencies != null)
1155       {
1156         Matcher matcher = SPLIT.matcher(scanDependencies);
1157         while (matcher.find())
1158         {
1159           getLog().info("Adding dependencies from scope " + matcher.group() + " to the list of roots to scan");
1160           for (Artifact artifact : project.getDependencyArtifacts())
1161           {
1162             if (!artifact.getScope().equalsIgnoreCase(matcher.group()))
1163               continue;
1164             if (artifact.getFile() == null)
1165             {
1166               getLog().warn("Cannot add dependency " + artifact.getId() + ": no JAR-file available!");
1167               continue;
1168             }
1169             getLog().info("Adding dependencies from scope " + artifact.getId() + " to the list of roots to scan");
1170             urls.add(artifact.getFile().toURI().toURL());
1171           }
1172         }
1173       }
1174     }
1175     catch (MalformedURLException e)
1176     {
1177       getLog().error("Error while adding dependencies to the list of roots to scan!", e);
1178       throw new MojoFailureException(e.getMessage());
1179     }
1180   }
1181
1182   private Set<String> scanUrls(Set<URL> scanRoots)
1183       throws
1184         MojoFailureException
1185   {
1186     try
1187     {
1188       AnnotationDB db = new AnnotationDB();
1189       for (URL root : scanRoots)
1190         db.scanArchives(root);
1191
1192       Set<String> classes = new HashSet<String>();
1193       if (db.getAnnotationIndex().containsKey(Entity.class.getName()))
1194         classes.addAll(db.getAnnotationIndex().get(Entity.class.getName()));
1195       if (db.getAnnotationIndex().containsKey(MappedSuperclass.class.getName()))
1196         classes.addAll(db.getAnnotationIndex().get(MappedSuperclass.class.getName()));
1197       if (db.getAnnotationIndex().containsKey(Embeddable.class.getName()))
1198         classes.addAll(db.getAnnotationIndex().get(Embeddable.class.getName()));
1199
1200       return classes;
1201     }
1202     catch (Exception e)
1203     {
1204       getLog().error("Error while scanning!", e);
1205       throw new MojoFailureException(e.getMessage());
1206     }
1207   }
1208
1209   private void addAnnotated(
1210       String name,
1211       MetadataSources sources,
1212       ClassLoaderService classLoaderService,
1213       ModificationTracker tracker
1214       )
1215       throws
1216         MojoFailureException,
1217         MojoExecutionException
1218   {
1219     try
1220     {
1221       getLog().info("Adding annotated resource: " + name);
1222       String packageName = null;
1223
1224       boolean error = false;
1225       try
1226       {
1227         Class<?> annotatedClass = classLoaderService.classForName(name);
1228         String resourceName = annotatedClass.getName();
1229         resourceName =
1230             resourceName.substring(
1231                 resourceName.lastIndexOf(".") + 1,
1232                 resourceName.length()
1233                 ) + ".class";
1234         InputStream is = annotatedClass.getResourceAsStream(resourceName);
1235         if (is != null)
1236         {
1237           if (tracker.track(name, is))
1238             getLog().debug("New or modified class: " + name);
1239           else
1240             getLog().debug("Unchanged class: " + name);
1241           sources.addAnnotatedClass(annotatedClass);
1242           packageName = annotatedClass.getPackage().getName();
1243         }
1244         else
1245         {
1246           getLog().error("cannot find ressource " + resourceName + " for class " + name);
1247           error = true;
1248         }
1249       }
1250       catch(ClassLoadingException e)
1251       {
1252         packageName = name;
1253       }
1254       if (error)
1255       {
1256         throw new MojoExecutionException("error while inspecting annotated class " + name);
1257       }
1258
1259       while (packageName != null)
1260       {
1261         if (packages.contains(packageName))
1262           return;
1263         String resource = packageName.replace('.', '/') + "/package-info.class";
1264         InputStream is = classLoaderService.locateResourceStream(resource);
1265         if (is == null)
1266         {
1267           // No compiled package-info available: no package-level annotations!
1268           getLog().debug("Package " + packageName + " is not annotated.");
1269         }
1270         else
1271         {
1272           if (tracker.track(packageName, is))
1273             getLog().debug("New or modified package: " + packageName);
1274           else
1275            getLog().debug("Unchanged package: " + packageName);
1276           getLog().info("Adding annotations from package " + packageName);
1277           sources.addPackage(packageName);
1278         }
1279         packages.add(packageName);
1280         int i = packageName.lastIndexOf('.');
1281         if (i < 0)
1282           packageName = null;
1283         else
1284           packageName = packageName.substring(0,i);
1285       }
1286     }
1287     catch (Exception e)
1288     {
1289       getLog().error("Error while adding the annotated class " + name, e);
1290       throw new MojoFailureException(e.getMessage());
1291     }
1292   }
1293
1294   private ParsedPersistenceXmlDescriptor loadPersistenceUnit(
1295       ClassLoaderService classLoaderService,
1296       Properties properties
1297       )
1298       throws
1299         MojoFailureException
1300   {
1301     PersistenceXmlParser parser =
1302         new PersistenceXmlParser(
1303             classLoaderService,
1304             PersistenceUnitTransactionType.RESOURCE_LOCAL
1305              );
1306
1307     Map<String, ParsedPersistenceXmlDescriptor> units =
1308         parser.doResolve(properties);
1309
1310     if (persistenceUnit == null)
1311     {
1312       Iterator<String> names = units.keySet().iterator();
1313       if (!names.hasNext())
1314       {
1315         getLog().info("Found no META-INF/persistence.xml.");
1316         return null;
1317       }
1318
1319       String name = names.next();
1320       if (!names.hasNext())
1321       {
1322           getLog().info("Using persistence-unit " + name);
1323           return units.get(name);
1324       }
1325
1326       StringBuilder builder = new StringBuilder();
1327       builder.append("No name provided and multiple persistence units found: ");
1328       builder.append(name);
1329       while(names.hasNext())
1330       {
1331         builder.append(", ");
1332         builder.append(names.next());
1333       }
1334       builder.append('.');
1335       throw new MojoFailureException(builder.toString());
1336     }
1337
1338     if (units.containsKey(persistenceUnit))
1339     {
1340       getLog().info("Using configured persistence-unit " + persistenceUnit);
1341       return units.get(persistenceUnit);
1342     }
1343
1344     throw new MojoFailureException("Could not find persistence-unit " + persistenceUnit);
1345   }
1346 }