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