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