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