Upgraded Hibernate
[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 and merging configuration */
489       properties.putAll(loadProperties(configLoader));
490       LoadedConfig config = loadConfig(configLoader);
491       if (config != null)
492         properties.putAll(config.getConfigurationValues());
493       ParsedPersistenceXmlDescriptor unit = loadPersistenceUnit(properties);
494       if (unit != null)
495         properties.putAll(unit.getProperties());
496
497       /** Overwriting/Completing configuration */
498       configure(properties, tracker);
499
500       /** Check configuration for modifications */
501       if(tracker.track(properties))
502         getLog().debug("Configuration has changed.");
503       else
504         getLog().debug("Configuration unchanged.");
505
506       /** Check, that the outputfile is writable */
507       final File output = getOutputFile(filename);
508       /** Check, if the outputfile is missing or was changed */
509       checkOutputFile(output, tracker);
510
511       /** Configure Hibernate */
512       final StandardServiceRegistry serviceRegistry =
513           new StandardServiceRegistryBuilder(bootstrapServiceRegitry)
514               .applySettings(properties)
515               .addService(ConnectionProvider.class, connectionProvider)
516               .build();
517       final MetadataSources sources = new MetadataSources(serviceRegistry);
518
519       /** Add the remaining class-path-elements */
520       completeClassPath(classLoader);
521
522       /** Apply mappings from hibernate-configuration, if present */
523       if (config != null)
524       {
525         for (MappingReference mapping : config.getMappingReferences())
526           mapping.apply(sources);
527       }
528
529       Set<String> classes;
530       if (unit == null)
531       {
532         /** No persistent unit: default behaviour */
533         if (scanClasses == null)
534           scanClasses = true;
535         Set<URL> urls = new HashSet<URL>();
536         if (scanClasses)
537           addRoot(urls, outputDirectory);
538         if (scanTestClasses)
539           addRoot(urls, testOutputDirectory);
540         addDependencies(urls);
541         classes = scanUrls(urls);
542       }
543       else
544       {
545         /** Follow configuration in persisten unit */
546         if (scanClasses == null)
547           scanClasses = !unit.isExcludeUnlistedClasses();
548         Set<URL> urls = new HashSet<URL>();
549         if (scanClasses)
550         {
551           /**
552            * Scan the root of the persiten unit and configured jars for
553            * annotated classes
554            */
555           urls.add(unit.getPersistenceUnitRootUrl());
556           for (URL url : unit.getJarFileUrls())
557             urls.add(url);
558         }
559         if (scanTestClasses)
560           addRoot(urls, testOutputDirectory);
561         classes = scanUrls(urls);
562         for (String className : unit.getManagedClassNames())
563           classes.add(className);
564         /**
565          * Add mappings from the default mapping-file
566          * <code>META-INF/orm.xml</code>, if present
567          */
568         boolean error = false;
569         InputStream is;
570         is = classLoader.getResourceAsStream("META-INF/orm.xml");
571         if (is != null)
572         {
573           getLog().info("Adding default JPA-XML-mapping from META-INF/orm.xml");
574           try
575           {
576             tracker.track("META-INF/orm.xml", is);
577             sources.addResource("META-INF/orm.xml");
578           }
579           catch (IOException e)
580           {
581             getLog().error("cannot read META-INF/orm.xml: " + e);
582             error = true;
583           }
584         }
585         else
586         {
587           getLog().debug("no META-INF/orm.xml found");
588         }
589         /**
590          * Add mappings from files, that are explicitly configured in the
591          * persistence unit
592          */
593         for (String mapping : unit.getMappingFileNames())
594         {
595           getLog().info("Adding explicitly configured mapping from " + mapping);
596           is = classLoader.getResourceAsStream(mapping);
597           if (is != null)
598           {
599             try
600             {
601               tracker.track(mapping, is);
602               sources.addResource(mapping);
603             }
604             catch (IOException e)
605             {
606               getLog().info("cannot read mapping-file " + mapping + ": " + e);
607               error = true;
608             }
609           }
610           else
611           {
612             getLog().error("cannot find mapping-file " + mapping);
613             error = true;
614           }
615         }
616         if (error)
617           throw new MojoFailureException(
618               "error, while reading mappings configured in persistence-unit \"" +
619               unit.getName() +
620               "\""
621               );
622       }
623
624       /** Add the configured/collected annotated classes */
625       for (String className : classes)
626         addAnnotated(className, sources, classLoaderService, tracker);
627
628       /** Add explicitly configured classes */
629       addMappings(sources, tracker);
630
631       /** Skip execution, if mapping and configuration is unchanged */
632       if (!tracker.modified())
633       {
634         getLog().info("Mapping and configuration unchanged.");
635         if (force)
636           getLog().info("Generation/execution is forced!");
637         else
638         {
639           getLog().info("Skipping schema generation!");
640           project.getProperties().setProperty(SKIPPED, "true");
641           return;
642         }
643       }
644
645
646       /** Truncate output file */
647       try
648       {
649         new FileOutputStream(output).getChannel().truncate(0).close();
650       }
651       catch (IOException e)
652       {
653         String error =
654             "Error while truncating " + output.getAbsolutePath() + ": "
655             + e.getMessage();
656         getLog().warn(error);
657         throw new MojoExecutionException(error);
658       }
659
660       /** Create a connection, if sufficient configuration infromation is available */
661       connectionProvider.open(classLoaderService, properties);
662
663       MetadataBuilder metadataBuilder = sources.getMetadataBuilder();
664
665       StrategySelector strategySelector =
666           serviceRegistry.getService(StrategySelector.class);
667
668       if (properties.containsKey(IMPLICIT_NAMING_STRATEGY))
669       {
670         metadataBuilder.applyImplicitNamingStrategy(
671             strategySelector.resolveStrategy(
672                 ImplicitNamingStrategy.class,
673                 properties.getProperty(IMPLICIT_NAMING_STRATEGY)
674                 )
675             );
676       }
677
678       if (properties.containsKey(PHYSICAL_NAMING_STRATEGY))
679       {
680         metadataBuilder.applyPhysicalNamingStrategy(
681             strategySelector.resolveStrategy(
682                 PhysicalNamingStrategy.class,
683                 properties.getProperty(PHYSICAL_NAMING_STRATEGY)
684                 )
685             );
686       }
687
688       /** Prepare the generation of the SQL */
689       Map settings = new HashMap();
690       settings.putAll(
691           serviceRegistry
692               .getService(ConfigurationService.class)
693               .getSettings()
694               );
695       ExceptionHandlerCollectingImpl handler =
696           new ExceptionHandlerCollectingImpl();
697       ExecutionOptions options =
698           SchemaManagementToolCoordinator
699               .buildExecutionOptions(settings, handler);
700       final EnumSet<TargetType> targetTypes = EnumSet.of(TargetType.SCRIPT);
701       if (execute)
702         targetTypes.add(TargetType.DATABASE);
703       TargetDescriptor target = new TargetDescriptor()
704       {
705         @Override
706         public EnumSet<TargetType> getTargetTypes()
707         {
708           return targetTypes;
709         }
710
711         @Override
712         public ScriptTargetOutput getScriptTargetOutput()
713         {
714           String charset =
715               (String)
716               serviceRegistry
717                   .getService(ConfigurationService.class)
718                   .getSettings()
719                   .get(AvailableSettings.HBM2DDL_CHARSET_NAME);
720           return new ScriptTargetOutputToFile(output, charset);
721         }
722       };
723
724       /**
725        * Change class-loader of current thread.
726        * This is necessary, because still not all parts of Hibernate 5 use
727        * the newly introduced ClassLoaderService and will fail otherwise!
728        */
729       Thread thread = Thread.currentThread();
730       ClassLoader contextClassLoader = thread.getContextClassLoader();
731       try
732       {
733         thread.setContextClassLoader(classLoader);
734         build((MetadataImplementor)metadataBuilder.build(), options, target);
735         if (handler.getExceptions().size() > 0)
736         {
737           StringBuilder builder = new StringBuilder();
738           builder.append("Hibernate failed:");
739           for (Exception e : handler.getExceptions())
740           {
741             builder.append("\n * ");
742             builder.append(e.getMessage());
743             AbstractSchemaMojo.printStrackTrace(builder, e);
744             builder.append("\n");
745           }
746           String error = builder.toString();
747           getLog().error(error);
748           throw new MojoFailureException(error);
749         }
750       }
751       finally
752       {
753         thread.setContextClassLoader(contextClassLoader);
754         /** Track, the content of the generated script */
755         checkOutputFile(output, tracker);
756       }
757     }
758     catch (MojoExecutionException e)
759     {
760       tracker.failed();
761       throw e;
762     }
763     catch (MojoFailureException e)
764     {
765       tracker.failed();
766       throw e;
767     }
768     catch (RuntimeException e)
769     {
770       tracker.failed();
771       throw e;
772     }
773     finally
774     {
775       /** Remember mappings and configuration */
776       tracker.save();
777
778       /** Close the connection - if one was opened */
779       connectionProvider.close();
780
781       /** Stop Log-Capturing */
782       MavenLogAppender.endPluginLog(this);
783     }
784   }
785
786
787   abstract void build(
788       MetadataImplementor metadata,
789       ExecutionOptions options,
790       TargetDescriptor target
791       )
792     throws
793       MojoFailureException,
794       MojoExecutionException;
795
796
797   private MutableClassLoader createClassLoader() throws MojoExecutionException
798   {
799     try
800     {
801       getLog().debug("Creating ClassLoader for project-dependencies...");
802       LinkedHashSet<URL> urls = new LinkedHashSet<URL>();
803       File file;
804
805       file = new File(testOutputDirectory);
806       if (!file.exists())
807       {
808         getLog().info("Creating test-output-directory: " + testOutputDirectory);
809         file.mkdirs();
810       }
811       urls.add(file.toURI().toURL());
812
813       file = new File(outputDirectory);
814       if (!file.exists())
815       {
816         getLog().info("Creating output-directory: " + outputDirectory);
817         file.mkdirs();
818       }
819       urls.add(file.toURI().toURL());
820
821       return new MutableClassLoader(urls, getLog());
822     }
823     catch (Exception e)
824     {
825       getLog().error("Error while creating ClassLoader!", e);
826       throw new MojoExecutionException(e.getMessage());
827     }
828   }
829
830   private void completeClassPath(MutableClassLoader classLoader)
831       throws
832         MojoExecutionException
833   {
834     try
835     {
836       getLog().debug("Completing class-paths of the ClassLoader for project-dependencies...");
837       List<String> classpathFiles = project.getCompileClasspathElements();
838       if (scanTestClasses)
839         classpathFiles.addAll(project.getTestClasspathElements());
840       LinkedHashSet<URL> urls = new LinkedHashSet<URL>();
841       for (String pathElement : classpathFiles)
842       {
843         getLog().debug("Dependency: " + pathElement);
844         urls.add(new File(pathElement).toURI().toURL());
845       }
846       classLoader.add(urls);
847     }
848     catch (Exception e)
849     {
850       getLog().error("Error while creating ClassLoader!", e);
851       throw new MojoExecutionException(e.getMessage());
852     }
853   }
854
855   private Map loadProperties(ConfigLoader configLoader)
856       throws
857         MojoExecutionException
858   {
859     /** Try to read configuration from properties-file */
860     if (hibernateProperties == null)
861     {
862       try
863       {
864         return configLoader.loadProperties("hibernate.properties");
865       }
866       catch (ConfigurationException e)
867       {
868         getLog().debug(e.getMessage());
869         return Collections.EMPTY_MAP;
870       }
871     }
872     else
873     {
874       try
875       {
876         File file = new File(hibernateProperties);
877         if (file.exists())
878         {
879           getLog().info("Reading settings from file " + hibernateProperties + "...");
880           return configLoader.loadProperties(file);
881         }
882         else
883           return configLoader.loadProperties(hibernateProperties);
884       }
885       catch (ConfigurationException e)
886       {
887         getLog().error("Error while reading properties!", e);
888         throw new MojoExecutionException(e.getMessage());
889       }
890     }
891   }
892
893   private LoadedConfig loadConfig(ConfigLoader configLoader)
894       throws MojoExecutionException
895   {
896     /** Try to read configuration from configuration-file */
897     if (hibernateConfig == null)
898     {
899       try
900       {
901         return configLoader.loadConfigXmlResource("hibernate.cfg.xml");
902       }
903       catch (ConfigurationException e)
904       {
905         getLog().debug(e.getMessage());
906         return null;
907       }
908     }
909     else
910     {
911       try
912       {
913         File file = new File(hibernateConfig);
914         if (file.exists())
915         {
916           getLog().info("Reading configuration from file " + hibernateConfig + "...");
917           return configLoader.loadConfigXmlFile(file);
918         }
919         else
920         {
921           return configLoader.loadConfigXmlResource(hibernateConfig);
922         }
923       }
924       catch (ConfigurationException e)
925       {
926         getLog().error("Error while reading configuration!", e);
927         throw new MojoExecutionException(e.getMessage());
928       }
929     }
930   }
931
932   private void configure(Properties properties, ModificationTracker tracker)
933       throws MojoFailureException
934   {
935     /**
936      * Special treatment for the configuration-value "execute": if it is
937      * switched to "true", the genearation fo the schema should be forced!
938      */
939     if (tracker.check(EXECUTE, execute.toString()) && execute)
940     {
941       getLog().info(
942           "hibernate.schema.execute was switched on: " +
943           "forcing generation/execution of SQL"
944           );
945       tracker.touch();
946     }
947     configure(properties, execute, EXECUTE);
948
949     /**
950      * Configure the generation of the SQL.
951      * Overwrite values from properties-file if the configuration parameter is
952      * known to Hibernate.
953      */
954     configure(properties, dialect, DIALECT);
955     configure(properties, delimiter, HBM2DDL_DELIMITER);
956     configure(properties, format, FORMAT_SQL);
957     configure(properties, createNamespaces, HBM2DLL_CREATE_NAMESPACES);
958     configure(properties, implicitNamingStrategy, IMPLICIT_NAMING_STRATEGY);
959     configure(properties, physicalNamingStrategy, PHYSICAL_NAMING_STRATEGY);
960     configure(properties, outputDirectory, OUTPUTDIRECTORY);
961     configure(properties, scanDependencies, SCAN_DEPENDENCIES);
962     configure(properties, scanTestClasses, SCAN_TESTCLASSES);
963     configure(properties, testOutputDirectory, TEST_OUTPUTDIRECTORY);
964
965     /**
966      * Special treatment for the configuration-value "show": a change of its
967      * configured value should not lead to a regeneration of the database
968      * schama!
969      */
970     if (show == null)
971       show = Boolean.valueOf(properties.getProperty(SHOW_SQL));
972     else
973       properties.setProperty(SHOW_SQL, show.toString());
974
975     /**
976      * Configure the connection parameters.
977      * Overwrite values from properties-file.
978      */
979     configure(properties, driver, DRIVER, JPA_JDBC_DRIVER);
980     configure(properties, url, URL, JPA_JDBC_URL);
981     configure(properties, username, USER, JPA_JDBC_USER);
982     configure(properties, password, PASS, JPA_JDBC_PASSWORD);
983
984     if (properties.isEmpty())
985     {
986       getLog().error("No properties set!");
987       throw new MojoFailureException("Hibernate configuration is missing!");
988     }
989
990     getLog().info("Gathered configuration:");
991     for (Entry<Object,Object> entry : properties.entrySet())
992       getLog().info("  " + entry.getKey() + " = " + entry.getValue());
993   }
994
995   private void configure(
996       Properties properties,
997       String value,
998       String key,
999       String alternativeKey
1000       )
1001   {
1002     configure(properties, value, key);
1003
1004     if (properties.containsKey(alternativeKey))
1005     {
1006       if (properties.containsKey(key))
1007       {
1008         getLog().warn(
1009             "Ignoring property " + alternativeKey + "=\"" +
1010             properties.getProperty(alternativeKey) +
1011             "\" in favour for property " + key + "=\"" +
1012             properties.getProperty(key) + "\""
1013             );
1014         properties.remove(alternativeKey);
1015       }
1016       else
1017       {
1018         value = properties.getProperty(alternativeKey);
1019         properties.remove(alternativeKey);
1020         getLog().info(
1021             "Using value \"" + value + "\" from property " + alternativeKey +
1022             " for property " + key
1023             );
1024         properties.setProperty(key, value);
1025       }
1026     }
1027   }
1028
1029   private void configure(Properties properties, String value, String key)
1030   {
1031     if (value != null)
1032     {
1033       if (properties.containsKey(key))
1034       {
1035         if (!properties.getProperty(key).equals(value))
1036         {
1037           getLog().info(
1038               "Overwriting property " + key + "=\"" +
1039               properties.getProperty(key) +
1040               "\" with value \"" + value + "\""
1041               );
1042           properties.setProperty(key, value);
1043         }
1044       }
1045       else
1046       {
1047         getLog().debug("Using value \"" + value + "\" for property " + key);
1048         properties.setProperty(key, value);
1049       }
1050     }
1051   }
1052
1053   private void configure(Properties properties, Boolean value, String key)
1054   {
1055     configure(properties, value == null ? null : value.toString(), key);
1056   }
1057
1058   private File getOutputFile(String filename)
1059       throws
1060         MojoExecutionException
1061   {
1062     File output = new File(filename);
1063
1064     if (!output.isAbsolute())
1065     {
1066       // Interpret relative file path relative to build directory
1067       output = new File(buildDirectory, filename);
1068     }
1069     getLog().debug("Output file: " + output.getPath());
1070
1071     // Ensure that directory path for specified file exists
1072     File outFileParentDir = output.getParentFile();
1073     if (null != outFileParentDir && !outFileParentDir.exists())
1074     {
1075       try
1076       {
1077         getLog().info(
1078             "Creating directory path for output file:" +
1079             outFileParentDir.getPath()
1080             );
1081         outFileParentDir.mkdirs();
1082       }
1083       catch (Exception e)
1084       {
1085         String error =
1086             "Error creating directory path for output file: " + e.getMessage();
1087         getLog().error(error);
1088         throw new MojoExecutionException(error);
1089       }
1090     }
1091
1092     try
1093     {
1094       output.createNewFile();
1095     }
1096     catch (IOException e)
1097     {
1098       String error = "Error creating output file: " + e.getMessage();
1099       getLog().error(error);
1100       throw new MojoExecutionException(error);
1101     }
1102
1103     if (!output.canWrite())
1104     {
1105       String error =
1106           "Output file " + output.getAbsolutePath() + " is not writable!";
1107       getLog().error(error);
1108       throw new MojoExecutionException(error);
1109     }
1110
1111     return output;
1112   }
1113
1114   private void checkOutputFile(File output, ModificationTracker tracker)
1115       throws
1116         MojoExecutionException
1117   {
1118     try
1119     {
1120       if (output.exists())
1121         tracker.track(SCRIPT, new FileInputStream(output));
1122       else
1123         tracker.track(SCRIPT, ZonedDateTime.now().toString());
1124     }
1125     catch (IOException e)
1126     {
1127       String error =
1128           "Error while checking the generated script: " + e.getMessage();
1129       getLog().error(error);
1130       throw new MojoExecutionException(error);
1131     }
1132   }
1133
1134   private void addMappings(MetadataSources sources, ModificationTracker tracker)
1135       throws MojoFailureException
1136   {
1137     getLog().debug("Adding explicitly configured mappings...");
1138     if (mappings != null)
1139     {
1140       try
1141       {
1142         for (String filename : mappings.split("[\\s,]+"))
1143         {
1144           // First try the filename as absolute/relative path
1145           File file = new File(filename);
1146           if (!file.exists())
1147           {
1148             // If the file was not found, search for it in the resource-directories
1149             for (Resource resource : project.getResources())
1150             {
1151               file = new File(resource.getDirectory() + File.separator + filename);
1152               if (file.exists())
1153                 break;
1154             }
1155           }
1156           if (file.exists())
1157           {
1158             if (file.isDirectory())
1159               // TODO: add support to read all mappings under a directory
1160               throw new MojoFailureException(file.getAbsolutePath() + " is a directory");
1161             if (tracker.track(filename, new FileInputStream(file)))
1162               getLog().debug("Found new or modified mapping-file: " + filename);
1163             else
1164               getLog().debug("Mapping-file unchanged: " + filename);
1165
1166             sources.addFile(file);
1167           }
1168           else
1169             throw new MojoFailureException("File " + filename + " could not be found in any of the configured resource-directories!");
1170         }
1171       }
1172       catch (IOException e)
1173       {
1174         throw new MojoFailureException("Cannot calculate MD5 sums!", e);
1175       }
1176     }
1177   }
1178
1179   private void addRoot(Set<URL> urls, String path) throws MojoFailureException
1180   {
1181     try
1182     {
1183       File dir = new File(path);
1184       if (dir.exists())
1185       {
1186         getLog().info("Adding " + dir.getAbsolutePath() + " to the list of roots to scan...");
1187         urls.add(dir.toURI().toURL());
1188       }
1189       else
1190         getLog().warn(
1191             "the directory cannot be scanned for annotated classes, " +
1192             "because it does not exist: " +
1193             dir.getAbsolutePath()
1194             );
1195     }
1196     catch (MalformedURLException e)
1197     {
1198       getLog().error("error while adding the project-root to the list of roots to scan!", e);
1199       throw new MojoFailureException(e.getMessage());
1200     }
1201   }
1202
1203   private void addDependencies(Set<URL> urls) throws MojoFailureException
1204   {
1205     try
1206     {
1207       if (scanDependencies != null)
1208       {
1209         Matcher matcher = SPLIT.matcher(scanDependencies);
1210         while (matcher.find())
1211         {
1212           getLog().info("Adding dependencies from scope " + matcher.group() + " to the list of roots to scan");
1213           for (Artifact artifact : project.getDependencyArtifacts())
1214           {
1215             if (!artifact.getScope().equalsIgnoreCase(matcher.group()))
1216               continue;
1217             if (artifact.getFile() == null)
1218             {
1219               getLog().warn("Cannot add dependency " + artifact.getId() + ": no JAR-file available!");
1220               continue;
1221             }
1222             getLog().info("Adding dependencies from scope " + artifact.getId() + " to the list of roots to scan");
1223             urls.add(artifact.getFile().toURI().toURL());
1224           }
1225         }
1226       }
1227     }
1228     catch (MalformedURLException e)
1229     {
1230       getLog().error("Error while adding dependencies to the list of roots to scan!", e);
1231       throw new MojoFailureException(e.getMessage());
1232     }
1233   }
1234
1235   private Set<String> scanUrls(Set<URL> scanRoots)
1236       throws
1237         MojoFailureException
1238   {
1239     try
1240     {
1241       AnnotationDB db = new AnnotationDB();
1242       for (URL root : scanRoots)
1243         db.scanArchives(root);
1244
1245       Set<String> classes = new HashSet<String>();
1246       if (db.getAnnotationIndex().containsKey(Entity.class.getName()))
1247         classes.addAll(db.getAnnotationIndex().get(Entity.class.getName()));
1248       if (db.getAnnotationIndex().containsKey(MappedSuperclass.class.getName()))
1249         classes.addAll(db.getAnnotationIndex().get(MappedSuperclass.class.getName()));
1250       if (db.getAnnotationIndex().containsKey(Embeddable.class.getName()))
1251         classes.addAll(db.getAnnotationIndex().get(Embeddable.class.getName()));
1252
1253       return classes;
1254     }
1255     catch (Exception e)
1256     {
1257       getLog().error("Error while scanning!", e);
1258       throw new MojoFailureException(e.getMessage());
1259     }
1260   }
1261
1262   private void addAnnotated(
1263       String name,
1264       MetadataSources sources,
1265       ClassLoaderService classLoaderService,
1266       ModificationTracker tracker
1267       )
1268       throws
1269         MojoFailureException,
1270         MojoExecutionException
1271   {
1272     try
1273     {
1274       getLog().info("Adding annotated resource: " + name);
1275       String packageName = null;
1276
1277       boolean error = false;
1278       try
1279       {
1280         Class<?> annotatedClass = classLoaderService.classForName(name);
1281         String resourceName = annotatedClass.getName();
1282         resourceName =
1283             resourceName.substring(
1284                 resourceName.lastIndexOf(".") + 1,
1285                 resourceName.length()
1286                 ) + ".class";
1287         InputStream is = annotatedClass.getResourceAsStream(resourceName);
1288         if (is != null)
1289         {
1290           if (tracker.track(name, is))
1291             getLog().debug("New or modified class: " + name);
1292           else
1293             getLog().debug("Unchanged class: " + name);
1294           sources.addAnnotatedClass(annotatedClass);
1295           packageName = annotatedClass.getPackage().getName();
1296         }
1297         else
1298         {
1299           getLog().error("cannot find ressource " + resourceName + " for class " + name);
1300           error = true;
1301         }
1302       }
1303       catch(ClassLoadingException e)
1304       {
1305         packageName = name;
1306       }
1307       if (error)
1308       {
1309         throw new MojoExecutionException("error while inspecting annotated class " + name);
1310       }
1311
1312       while (packageName != null)
1313       {
1314         if (packages.contains(packageName))
1315           return;
1316         String resource = packageName.replace('.', '/') + "/package-info.class";
1317         InputStream is = classLoaderService.locateResourceStream(resource);
1318         if (is == null)
1319         {
1320           // No compiled package-info available: no package-level annotations!
1321           getLog().debug("Package " + packageName + " is not annotated.");
1322         }
1323         else
1324         {
1325           if (tracker.track(packageName, is))
1326             getLog().debug("New or modified package: " + packageName);
1327           else
1328            getLog().debug("Unchanged package: " + packageName);
1329           getLog().info("Adding annotations from package " + packageName);
1330           sources.addPackage(packageName);
1331         }
1332         packages.add(packageName);
1333         int i = packageName.lastIndexOf('.');
1334         if (i < 0)
1335           packageName = null;
1336         else
1337           packageName = packageName.substring(0,i);
1338       }
1339     }
1340     catch (Exception e)
1341     {
1342       getLog().error("Error while adding the annotated class " + name, e);
1343       throw new MojoFailureException(e.getMessage());
1344     }
1345   }
1346
1347   private ParsedPersistenceXmlDescriptor loadPersistenceUnit(
1348       Properties properties
1349       )
1350       throws
1351         MojoFailureException
1352   {
1353     List<ParsedPersistenceXmlDescriptor> units =
1354         PersistenceXmlParser.locatePersistenceUnits(properties);
1355
1356     if (persistenceUnit == null)
1357     {
1358       if (units.isEmpty())
1359       {
1360         getLog().info("Found no META-INF/persistence.xml.");
1361         return null;
1362       }
1363
1364       if (units.size() == 1)
1365       {
1366         ParsedPersistenceXmlDescriptor descriptor = units.get(0);
1367         getLog().info("Using persistence-unit " + descriptor.getName());
1368         return descriptor;
1369       }
1370
1371       StringBuilder builder = new StringBuilder();
1372       builder.append("No name provided and multiple persistence units found: ");
1373       Iterator<ParsedPersistenceXmlDescriptor> it = units.iterator();
1374       while(it.hasNext())
1375       {
1376         builder.append(it.next().getName());
1377         if (it.hasNext())
1378           builder.append(", ");
1379       }
1380       builder.append('.');
1381       throw new MojoFailureException(builder.toString());
1382     }
1383
1384     Iterator<ParsedPersistenceXmlDescriptor> it = units.iterator();
1385     while(it.hasNext())
1386     {
1387       ParsedPersistenceXmlDescriptor descriptor =  it.next();
1388       if (descriptor.getName().equals(persistenceUnit))
1389       {
1390         getLog().info("Using configured persistence-unit " + persistenceUnit);
1391         return descriptor;
1392       }
1393     }
1394
1395     throw new MojoFailureException("Could not find persistence-unit " + persistenceUnit);
1396   }
1397
1398
1399   public static void printStrackTrace(StringBuilder builder, Throwable t)
1400   {
1401     while (t != null)
1402     {
1403       builder.append("\n\tCause: ");
1404       builder.append(t.getMessage() == null ? "" : t.getMessage().replaceAll("\\s+", " "));
1405       for (StackTraceElement trace : t.getStackTrace())
1406       {
1407         builder.append("\n\t");
1408         builder.append(trace.getClassName());
1409         builder.append(".");
1410         builder.append(trace.getMethodName());
1411         builder.append("():");
1412         builder.append(trace.getLineNumber());
1413       }
1414       t = t.getCause();
1415     }
1416   }
1417 }