JPASchemaGenMojo.java

package ejava.utils.jpa;


import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.Persistence;

import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;

/**
 * This plugin will generate SQL schema for a specified persistence unit. It is targeted/tuned to 
 * have the features desired for use with the ejava course examples. Thus it inserts hibernate-specific
 * extensions to enable pretty-printing and line terminators. Use this as an example of how you could
 * create something more general purpose if you would like.
 */
@Mojo( name = "generate", defaultPhase = LifecyclePhase.PROCESS_TEST_CLASSES , requiresDependencyResolution = ResolutionScope.TEST, threadSafe=true  )
@Execute(phase=LifecyclePhase.TEST_COMPILE)
public class JPASchemaGenMojo extends AbstractMojo {
	
	/**
	 * The name of the persistence unit from within META-INF/persistence.xml. Only required
	 * if there are multiple persistence units within the project. Otherwise, it will use the
	 * only one found in the path.
	 */
    @Parameter( property = "persistenceUnit", required=false)
    private String persistenceUnit;
    
    /**
     * The path to write the create script.
     */
    @Parameter( property = "createPath", required=false, defaultValue="target/classes/ddl/${persistenceUnit}-create.ddl")
    private String createPath;
    
    /**
     * The path to write the drop script.
     */
    @Parameter( property = "dropPath", required=false, defaultValue="target/classes/ddl/${persistenceUnit}-drop.ddl")
    private String dropPath;
    
    /**
     * Statement termination string
     */
    @Parameter( property = "delimiter", required=false, defaultValue=";")
    private String delimiter;
    
    @Parameter( property = "format", required=false, defaultValue="true")
    private boolean format;
    
    @Parameter( property = "scriptsAction", required=false, defaultValue="drop-and-create")
    private String scriptsAction;
    
    /**
     * Alternate JDBC URL used only for schemagen plugin. For whatever reason, Hibernate
     * requires a database connection when generating database schema to file and 
     * unfortunately leaves a session hanging open and the database file locked when 
     * using a file-based database. This setting helps avoid DB locks
     * for file-based, local databases. The default is to use the H2 in-memory 
     * database.
     */
    @Parameter( property = "schemagenUrl", required=false, defaultValue="jdbc:h2:mem:")
    private String schemagenUrl;
    
    /**
     * Username for the alternate DB referenced by schemagenUrl.
     */
    @Parameter( property = "schemagenUser", required=false, defaultValue="")
    private String schemagenUser;
    
    /**
     * Password for the alternateDB referenced by schemagenUrl.
     */
    @Parameter( property = "schemagenPassword", required=false, defaultValue="")
    private String schemagenPassword;
    
    /**
     * JDBC driver for schemagen JDBC URL. Used only if schemagenUrl is supplied
     * and will default to the driver appropriate for the default value of schemagenUrl.
     */
    @Parameter( property = "schemagenDriver", required=false, defaultValue="org.h2.Driver")
    private String schemagenDriver;

    /**
     * Describes the entire project.
     */
    @Parameter( property = "project", required=true, defaultValue = "${project}" )
    private MavenProject project;
    
    protected URLClassLoader getClassLoader() throws DependencyResolutionRequiredException, MalformedURLException {
		List<String> elements = project.getTestClasspathElements();
		URL[] urls = new URL[elements.size()];
		for (int i=0; i<elements.size(); i++) {
			String path = elements.get(i);
			URL url = new File(path).toURI().toURL();
			urls[i] = url;
		}
		URLClassLoader classLoader = new URLClassLoader(urls, getClass().getClassLoader());
		for (URL url: classLoader.getURLs()) {
			getLog().debug("url=" + url.toString());
		}
		return classLoader;
    }
    
    protected String resolvePath(String path) {
    		path = path.replace("${persistenceUnit}", persistenceUnit);
    		File f = new File(path);
    		//return !path.startsWith("/") ? project.getBasedir() + File.separator + path : path;
            return !f.isAbsolute() ? project.getBasedir() + File.separator + path : path;
    }
    
    protected Map<String, Object> configure() {
    		Map<String, Object> properties = new HashMap<>();
    		properties.put(AvailableSettings.HBM2DDL_SCRIPTS_ACTION, scriptsAction);
    		properties.put(AvailableSettings.HBM2DDL_SCRIPTS_CREATE_TARGET, resolvePath(createPath));
    		properties.put(AvailableSettings.HBM2DDL_SCRIPTS_DROP_TARGET, resolvePath(dropPath));
    		properties.put(AvailableSettings.HBM2DDL_DELIMITER, delimiter);
    		properties.put(AvailableSettings.FORMAT_SQL, Boolean.valueOf(format).toString());
    		if (schemagenUrl!=null && !schemagenUrl.trim().isEmpty()) {
    		    properties.put(AvailableSettings.JPA_JDBC_URL, schemagenUrl);
                properties.put(AvailableSettings.JPA_JDBC_USER, schemagenUser);
                properties.put(AvailableSettings.JPA_JDBC_PASSWORD, schemagenPassword);
                properties.put(AvailableSettings.JPA_JDBC_DRIVER, schemagenDriver);
    		}
    		return properties;
    }
    
    public void execute() throws MojoFailureException {
    		URLClassLoader classLoader = null;
    		try {
    			classLoader = getClassLoader();
    			this.persistenceUnit = findPersistenceUnit(classLoader);
    			getLog().info("Generating database schema for: " + persistenceUnit);
			
			Thread.currentThread().setContextClassLoader(classLoader);
			URL pxml = classLoader.getResource("META-INF/persistence.xml");
			URL hprops = classLoader.getResource("hibernate.properties");
			getLog().debug("META-INF/persistence.xml found= " + (pxml!=null ? pxml : "false"));
			getLog().debug("hibernate.properties found= " + (hprops!=null ? hprops : "false"));
			
			Map<String, Object> properties = configure();
			properties.forEach((k,v) -> getLog().debug(k + "=" + v));

			//hibernate has been appending to existing files
			for (String prop: Arrays.asList(AvailableSettings.HBM2DDL_SCRIPTS_DROP_TARGET, AvailableSettings.HBM2DDL_SCRIPTS_CREATE_TARGET)) {
			    String path = (String)properties.get(prop);
			    if (path!=null && path.toLowerCase().contains("target")) {
			        File f = new File(path);
			        if (f.exists()) {
    			        getLog().info("removing existing target file:" + f.getPath());
    			        f.delete();
			        }
		            //make sure parent directory exists
			        boolean created = f.getParentFile().mkdirs();
			        if (created) {
			            getLog().info("created missing schema target directory: " + f.getParent());
			        }
			    }
			}
			
			Persistence.generateSchema(persistenceUnit, properties);
			loadBeforeClosing();			
		} catch (DependencyResolutionRequiredException | MalformedURLException e) {
			throw new MojoFailureException(e.toString());
		} finally {
			if (classLoader!=null) {
				try { classLoader.close(); } catch (IOException e) {}
			}
		}
    }
    
    protected String findPersistenceUnit(ClassLoader clsLoader) throws MojoFailureException {
    		if (persistenceUnit!=null) {
    			return persistenceUnit;
    		}
    		Map<String, Object> properties = new HashMap<>();
    		properties.put(AvailableSettings.CLASSLOADERS, Collections.singletonList(clsLoader));
    		List<ParsedPersistenceXmlDescriptor> units = PersistenceXmlParser.locatePersistenceUnits(properties);
    		if (units.size()==1) {
    			return units.get(0).getName();
    		} else if (units.isEmpty()) {
    			throw new MojoFailureException("no persistenceUnit name specified and none found");
    		} else {
    			StringBuilder names = new StringBuilder();
    			units.forEach(n -> {
    				if (names.length()>0) { names.append(", "); } 
    				names.append(n.getName());
    			});
    			throw new MojoFailureException(String.format("too many persistence units found[%s], specify persistenceUnit name to use", names));    			
    		}
    }

    /**
     * kludge to try to avoid an ugly non-fatal stack trace of missing classes 
     * when plugin shuts down (closing the classloader) and the database attempts
     * to load new classes to complete its thread shutdown.
     * @throws MojoFailureException
     */
    protected void loadBeforeClosing() throws MojoFailureException {
    		for (String cls : new String[] {
    				"org.h2.mvstore.WriteBuffer",
    				"org.h2.mvstore.MVMap$2",
    				"org.h2.mvstore.MVMap$2$1",
    				"org.h2.mvstore.DataUtils$MapEntry",
    				"org.h2.mvstore.Chunk"
    				}) {
    			try {
    				Thread.currentThread().getContextClassLoader().loadClass(cls);
    			} catch (ClassNotFoundException ex) {
    				getLog().info("error pre-loading class[" + cls + "]: "+ ex.toString());
    				//throw new MojoFailureException("error pre-loading class[" + cls + "]: "+ ex.toString());
    			}
    		}
    		
    }

    public void setPersistenceUnit(String puName) {
		this.persistenceUnit = puName;
	}
	public void setProject(MavenProject project) {
		this.project = project;
	}

	public void setCreatePath(String createPath) {
		this.createPath = createPath;
	}

	public void setDropPath(String dropPath) {
		this.dropPath = dropPath;
	}

	public void setDelimiter(String delimiter) {
		this.delimiter = delimiter;
	}

	public void setFormat(boolean format) {
		this.format = format;
	}

	public void setScriptsAction(String scriptsAction) {
		this.scriptsAction = scriptsAction;
	}

}