View Javadoc
1   package ejava.utils.jpa;
2   
3   
4   import java.io.File;
5   import java.io.IOException;
6   import java.net.MalformedURLException;
7   import java.net.URL;
8   import java.net.URLClassLoader;
9   import java.util.Arrays;
10  import java.util.Collections;
11  import java.util.HashMap;
12  import java.util.List;
13  import java.util.Map;
14  
15  import javax.persistence.Persistence;
16  
17  import org.apache.maven.artifact.DependencyResolutionRequiredException;
18  import org.apache.maven.plugin.AbstractMojo;
19  import org.apache.maven.plugin.MojoFailureException;
20  import org.apache.maven.plugins.annotations.Execute;
21  import org.apache.maven.plugins.annotations.LifecyclePhase;
22  import org.apache.maven.plugins.annotations.Mojo;
23  import org.apache.maven.plugins.annotations.Parameter;
24  import org.apache.maven.plugins.annotations.ResolutionScope;
25  import org.apache.maven.project.MavenProject;
26  import org.hibernate.cfg.AvailableSettings;
27  import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
28  import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
29  
30  /**
31   * This plugin will generate SQL schema for a specified persistence unit. It is targeted/tuned to 
32   * have the features desired for use with the ejava course examples. Thus it inserts hibernate-specific
33   * extensions to enable pretty-printing and line terminators. Use this as an example of how you could
34   * create something more general purpose if you would like.
35   */
36  @Mojo( name = "generate", defaultPhase = LifecyclePhase.PROCESS_TEST_CLASSES , requiresDependencyResolution = ResolutionScope.TEST, threadSafe=true  )
37  @Execute(phase=LifecyclePhase.TEST_COMPILE)
38  public class JPASchemaGenMojo extends AbstractMojo {
39  	
40  	/**
41  	 * The name of the persistence unit from within META-INF/persistence.xml. Only required
42  	 * if there are multiple persistence units within the project. Otherwise, it will use the
43  	 * only one found in the path.
44  	 */
45      @Parameter( property = "persistenceUnit", required=false)
46      private String persistenceUnit;
47      
48      /**
49       * The path to write the create script.
50       */
51      @Parameter( property = "createPath", required=false, defaultValue="target/classes/ddl/${persistenceUnit}-create.ddl")
52      private String createPath;
53      
54      /**
55       * The path to write the drop script.
56       */
57      @Parameter( property = "dropPath", required=false, defaultValue="target/classes/ddl/${persistenceUnit}-drop.ddl")
58      private String dropPath;
59      
60      /**
61       * Statement termination string
62       */
63      @Parameter( property = "delimiter", required=false, defaultValue=";")
64      private String delimiter;
65      
66      @Parameter( property = "format", required=false, defaultValue="true")
67      private boolean format;
68      
69      @Parameter( property = "scriptsAction", required=false, defaultValue="drop-and-create")
70      private String scriptsAction;
71      
72      /**
73       * Alternate JDBC URL used only for schemagen plugin. For whatever reason, Hibernate
74       * requires a database connection when generating database schema to file and 
75       * unfortunately leaves a session hanging open and the database file locked when 
76       * using a file-based database. This setting helps avoid DB locks
77       * for file-based, local databases. The default is to use the H2 in-memory 
78       * database.
79       */
80      @Parameter( property = "schemagenUrl", required=false, defaultValue="jdbc:h2:mem:")
81      private String schemagenUrl;
82      
83      /**
84       * Username for the alternate DB referenced by schemagenUrl.
85       */
86      @Parameter( property = "schemagenUser", required=false, defaultValue="")
87      private String schemagenUser;
88      
89      /**
90       * Password for the alternateDB referenced by schemagenUrl.
91       */
92      @Parameter( property = "schemagenPassword", required=false, defaultValue="")
93      private String schemagenPassword;
94      
95      /**
96       * JDBC driver for schemagen JDBC URL. Used only if schemagenUrl is supplied
97       * and will default to the driver appropriate for the default value of schemagenUrl.
98       */
99      @Parameter( property = "schemagenDriver", required=false, defaultValue="org.h2.Driver")
100     private String schemagenDriver;
101 
102     /**
103      * Describes the entire project.
104      */
105     @Parameter( property = "project", required=true, defaultValue = "${project}" )
106     private MavenProject project;
107     
108     protected URLClassLoader getClassLoader() throws DependencyResolutionRequiredException, MalformedURLException {
109 		List<String> elements = project.getTestClasspathElements();
110 		URL[] urls = new URL[elements.size()];
111 		for (int i=0; i<elements.size(); i++) {
112 			String path = elements.get(i);
113 			URL url = new File(path).toURI().toURL();
114 			urls[i] = url;
115 		}
116 		URLClassLoader classLoader = new URLClassLoader(urls, getClass().getClassLoader());
117 		for (URL url: classLoader.getURLs()) {
118 			getLog().debug("url=" + url.toString());
119 		}
120 		return classLoader;
121     }
122     
123     protected String resolvePath(String path) {
124     		path = path.replace("${persistenceUnit}", persistenceUnit);
125     		File f = new File(path);
126     		//return !path.startsWith("/") ? project.getBasedir() + File.separator + path : path;
127             return !f.isAbsolute() ? project.getBasedir() + File.separator + path : path;
128     }
129     
130     protected Map<String, Object> configure() {
131     		Map<String, Object> properties = new HashMap<>();
132     		properties.put(AvailableSettings.HBM2DDL_SCRIPTS_ACTION, scriptsAction);
133     		properties.put(AvailableSettings.HBM2DDL_SCRIPTS_CREATE_TARGET, resolvePath(createPath));
134     		properties.put(AvailableSettings.HBM2DDL_SCRIPTS_DROP_TARGET, resolvePath(dropPath));
135     		properties.put(AvailableSettings.HBM2DDL_DELIMITER, delimiter);
136     		properties.put(AvailableSettings.FORMAT_SQL, Boolean.valueOf(format).toString());
137     		if (schemagenUrl!=null && !schemagenUrl.trim().isEmpty()) {
138     		    properties.put(AvailableSettings.JPA_JDBC_URL, schemagenUrl);
139                 properties.put(AvailableSettings.JPA_JDBC_USER, schemagenUser);
140                 properties.put(AvailableSettings.JPA_JDBC_PASSWORD, schemagenPassword);
141                 properties.put(AvailableSettings.JPA_JDBC_DRIVER, schemagenDriver);
142     		}
143     		return properties;
144     }
145     
146     public void execute() throws MojoFailureException {
147     		URLClassLoader classLoader = null;
148     		try {
149     			classLoader = getClassLoader();
150     			this.persistenceUnit = findPersistenceUnit(classLoader);
151     			getLog().info("Generating database schema for: " + persistenceUnit);
152 			
153 			Thread.currentThread().setContextClassLoader(classLoader);
154 			URL pxml = classLoader.getResource("META-INF/persistence.xml");
155 			URL hprops = classLoader.getResource("hibernate.properties");
156 			getLog().debug("META-INF/persistence.xml found= " + (pxml!=null ? pxml : "false"));
157 			getLog().debug("hibernate.properties found= " + (hprops!=null ? hprops : "false"));
158 			
159 			Map<String, Object> properties = configure();
160 			properties.forEach((k,v) -> getLog().debug(k + "=" + v));
161 
162 			//hibernate has been appending to existing files
163 			for (String prop: Arrays.asList(AvailableSettings.HBM2DDL_SCRIPTS_DROP_TARGET, AvailableSettings.HBM2DDL_SCRIPTS_CREATE_TARGET)) {
164 			    String path = (String)properties.get(prop);
165 			    if (path!=null && path.toLowerCase().contains("target")) {
166 			        File f = new File(path);
167 			        if (f.exists()) {
168     			        getLog().info("removing existing target file:" + f.getPath());
169     			        f.delete();
170 			        }
171 		            //make sure parent directory exists
172 			        boolean created = f.getParentFile().mkdirs();
173 			        if (created) {
174 			            getLog().info("created missing schema target directory: " + f.getParent());
175 			        }
176 			    }
177 			}
178 			
179 			Persistence.generateSchema(persistenceUnit, properties);
180 			loadBeforeClosing();			
181 		} catch (DependencyResolutionRequiredException | MalformedURLException e) {
182 			throw new MojoFailureException(e.toString());
183 		} finally {
184 			if (classLoader!=null) {
185 				try { classLoader.close(); } catch (IOException e) {}
186 			}
187 		}
188     }
189     
190     protected String findPersistenceUnit(ClassLoader clsLoader) throws MojoFailureException {
191     		if (persistenceUnit!=null) {
192     			return persistenceUnit;
193     		}
194     		Map<String, Object> properties = new HashMap<>();
195     		properties.put(AvailableSettings.CLASSLOADERS, Collections.singletonList(clsLoader));
196     		List<ParsedPersistenceXmlDescriptor> units = PersistenceXmlParser.locatePersistenceUnits(properties);
197     		if (units.size()==1) {
198     			return units.get(0).getName();
199     		} else if (units.isEmpty()) {
200     			throw new MojoFailureException("no persistenceUnit name specified and none found");
201     		} else {
202     			StringBuilder names = new StringBuilder();
203     			units.forEach(n -> {
204     				if (names.length()>0) { names.append(", "); } 
205     				names.append(n.getName());
206     			});
207     			throw new MojoFailureException(String.format("too many persistence units found[%s], specify persistenceUnit name to use", names));    			
208     		}
209     }
210 
211     /**
212      * kludge to try to avoid an ugly non-fatal stack trace of missing classes 
213      * when plugin shuts down (closing the classloader) and the database attempts
214      * to load new classes to complete its thread shutdown.
215      * @throws MojoFailureException
216      */
217     protected void loadBeforeClosing() throws MojoFailureException {
218     		for (String cls : new String[] {
219     				"org.h2.mvstore.WriteBuffer",
220     				"org.h2.mvstore.MVMap$2",
221     				"org.h2.mvstore.MVMap$2$1",
222     				"org.h2.mvstore.DataUtils$MapEntry",
223     				"org.h2.mvstore.Chunk"
224     				}) {
225     			try {
226     				Thread.currentThread().getContextClassLoader().loadClass(cls);
227     			} catch (ClassNotFoundException ex) {
228     				getLog().info("error pre-loading class[" + cls + "]: "+ ex.toString());
229     				//throw new MojoFailureException("error pre-loading class[" + cls + "]: "+ ex.toString());
230     			}
231     		}
232     		
233     }
234 
235     public void setPersistenceUnit(String puName) {
236 		this.persistenceUnit = puName;
237 	}
238 	public void setProject(MavenProject project) {
239 		this.project = project;
240 	}
241 
242 	public void setCreatePath(String createPath) {
243 		this.createPath = createPath;
244 	}
245 
246 	public void setDropPath(String dropPath) {
247 		this.dropPath = dropPath;
248 	}
249 
250 	public void setDelimiter(String delimiter) {
251 		this.delimiter = delimiter;
252 	}
253 
254 	public void setFormat(boolean format) {
255 		this.format = format;
256 	}
257 
258 	public void setScriptsAction(String scriptsAction) {
259 		this.scriptsAction = scriptsAction;
260 	}
261 
262 }