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