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