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