Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package io.github.divinespear.maven.plugin; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.net.URL; import java.net.URLClassLoader; import java.sql.DatabaseMetaData; import java.sql.Driver; import java.sql.DriverManager; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import javax.persistence.Persistence; import org.apache.commons.lang.NullArgumentException; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.resolver.ArtifactResolutionRequest; import org.apache.maven.artifact.resolver.ArtifactResolutionResult; import org.apache.maven.artifact.resolver.ArtifactResolver; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.plugin.logging.Log; import org.apache.maven.plugins.annotations.Component; 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.project.MavenProject; import org.apache.maven.settings.Settings; import org.codehaus.plexus.util.StringUtils; import org.eclipse.persistence.config.PersistenceUnitProperties; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.jpa.AvailableSettings; import org.hibernate.tool.hbm2ddl.SchemaExport; /** * Generate database schema or DDL scripts. * * @author divinespear */ @Mojo(name = "generate", defaultPhase = LifecyclePhase.PROCESS_CLASSES) public class JpaSchemaGeneratorMojo extends AbstractMojo { private final Log log = this.getLog(); @Component private MavenSession session; @Component private MavenProject project; @Component private MojoExecution mojo; @Component private ArtifactResolver resolver; // for Maven 3 only @Component private PluginDescriptor plugin; @Component private Settings settings; /** * skip schema generation */ @Parameter(property = "jpa-schema.generate.skip", required = true, defaultValue = "false") private boolean skip = false; public boolean isSkip() { return skip; } /** * generate as formatted */ @Parameter(property = "jpa-schema.generate.format", required = true, defaultValue = "false") private boolean format = false; public boolean isFormat() { return format; } /** * scan test classes */ @Parameter(property = "jpa-schema.generate.scan-test-classes", required = true, defaultValue = "false") private boolean scanTestClasses = false; public boolean isScanTestClasses() { return scanTestClasses; } /** * location of <code>persistence.xml</code> file * <p> * Note for Hibernate: <b>current version (4.3.1.Final) DOES NOT SUPPORT custom location.</b> ({@link SchemaExport} * support it, but JPA 2.1 schema generator does NOT.) */ @Parameter(required = true, defaultValue = PersistenceUnitProperties.ECLIPSELINK_PERSISTENCE_XML_DEFAULT) private String persistenceXml = PersistenceUnitProperties.ECLIPSELINK_PERSISTENCE_XML_DEFAULT; public String getPersistenceXml() { return persistenceXml; } /** * unit name of <code>persistence.xml</code> */ @Parameter(required = true, defaultValue = "default") private String persistenceUnitName = "default"; public String getPersistenceUnitName() { return persistenceUnitName; } /** * schema generation action for database * <p> * support value is <code>none</code>, <code>create</code>, <code>drop</code>, <code>drop-and-create</code>, or * <code>create-or-extend-tables</code>. * <p> * <code>create-or-extend-tables</code> only support for EclipseLink with database target. */ @Parameter(required = true, defaultValue = PersistenceUnitProperties.SCHEMA_GENERATION_NONE_ACTION) private String databaseAction = PersistenceUnitProperties.SCHEMA_GENERATION_NONE_ACTION; public String getDatabaseAction() { return databaseAction; } /** * schema generation action for script * <p> * support value is <code>none</code>, <code>create</code>, <code>drop</code>, or <code>drop-and-create</code>. */ @Parameter(required = true, defaultValue = PersistenceUnitProperties.SCHEMA_GENERATION_NONE_ACTION) private String scriptAction = PersistenceUnitProperties.SCHEMA_GENERATION_NONE_ACTION; public String getScriptAction() { return scriptAction; } /** * output directory for generated ddl scripts * <p> * REQUIRED for {@link #scriptAction} is one of <code>create</code>, <code>drop</code>, or * <code>drop-and-create</code>. */ @Parameter(defaultValue = "${project.build.directory}/generated-schema") private File outputDirectory; public File getOutputDirectory() { return outputDirectory; } /** * generated create script name * <p> * REQUIRED for {@link #scriptAction} is one of <code>create</code>, or <code>drop-and-create</code>. */ @Parameter(defaultValue = "create.sql") private String createOutputFileName = "create.sql"; public String getCreateOutputFileName() { return createOutputFileName; } public File getCreateOutputFile() { return this.outputDirectory == null ? null : new File(this.outputDirectory, this.createOutputFileName); } /** * generated drop script name * <p> * REQUIRED for {@link #scriptAction} is one of <code>drop</code>, or <code>drop-and-create</code>. */ @Parameter(defaultValue = "drop.sql") private String dropOutputFileName = "drop.sql"; public String getDropOutputFileName() { return dropOutputFileName; } public File getDropOutputFile() { return this.outputDirectory == null ? null : new File(this.outputDirectory, this.dropOutputFileName); } /** * specifies whether the creation of database artifacts is to occur on the basis of the object/relational mapping * metadata, DDL script, or a combination of the two. * <p> * support value is <code>metadata</code>, <code>script</code>, <code>metadata-then-script</code>, or * <code>script-then-metadata</code>. */ @Parameter(defaultValue = PersistenceUnitProperties.SCHEMA_GENERATION_METADATA_SOURCE) private String createSourceMode = PersistenceUnitProperties.SCHEMA_GENERATION_METADATA_SOURCE; public String getCreateSourceMode() { return createSourceMode; } /** * create source file path. * <p> * REQUIRED for {@link #createSourceMode} is one of <code>script</code>, <code>metadata-then-script</code>, or * <code>script-then-metadata</code>. */ @Parameter private File createSourceFile; public File getCreateSourceFile() { return createSourceFile; } /** * specifies whether the dropping of database artifacts is to occur on the basis of the object/relational mapping * metadata, DDL script, or a combination of the two. * <p> * support value is <code>metadata</code>, <code>script</code>, <code>metadata-then-script</code>, or * <code>script-then-metadata</code>. */ @Parameter(defaultValue = PersistenceUnitProperties.SCHEMA_GENERATION_METADATA_SOURCE) private String dropSourceMode = PersistenceUnitProperties.SCHEMA_GENERATION_METADATA_SOURCE; public String getDropSourceMode() { return dropSourceMode; } /** * drop source file path. * <p> * REQUIRED for {@link #dropSourceMode} is one of <code>script</code>, <code>metadata-then-script</code>, or * <code>script-then-metadata</code>. */ @Parameter private File dropSourceFile; public File getDropSourceFile() { return dropSourceFile; } /** * jdbc driver class name * <p> * default is declared class name in persistence xml. * <p> * and Remember, <strike><a href="http://callofduty.wikia.com/wiki/No_Russian" target="_blank">No * Russian</a></strike> you MUST configure jdbc driver as plugin's dependency. */ @Parameter private String jdbcDriver; public String getJdbcDriver() { return jdbcDriver; } /** * jdbc connection url * <p> * default is declared connection url in persistence xml. */ @Parameter private String jdbcUrl; public String getJdbcUrl() { return jdbcUrl; } /** * jdbc connection username * <p> * default is declared username in persistence xml. */ @Parameter private String jdbcUser; public String getJdbcUser() { return jdbcUser; } /** * jdbc connection password * <p> * default is declared password in persistence xml. * <p> * If your account has no password (especially local file-base, like Apache Derby, H2, etc...), it can be omitted. */ @Parameter private String jdbcPassword; public String getJdbcPassword() { return jdbcPassword; } /** * database product name for emulate database connection. this should useful for script-only action. * <ul> * <li>specified if scripts are to be generated by the persistence provider and a connection to the target database * is not supplied.</li> * <li>The value of this property should be the value returned for the target database by * {@link DatabaseMetaData#getDatabaseProductName()}</li> * </ul> */ @Parameter private String databaseProductName; public String getDatabaseProductName() { return databaseProductName; } /** * database major version for emulate database connection. this should useful for script-only action. * <ul> * <li>specified if sufficient database version information is not included from * {@link DatabaseMetaData#getDatabaseProductName()}</li> * <li>The value of this property should be the value returned for the target database by * {@link DatabaseMetaData#getDatabaseMajorVersion()}</li> * </ul> */ @Parameter private Integer databaseMajorVersion; public Integer getDatabaseMajorVersion() { return databaseMajorVersion; } /** * database minor version for emulate database connection. this should useful for script-only action. * <ul> * <li>specified if sufficient database version information is not included from * {@link DatabaseMetaData#getDatabaseProductName()}</li> * <li>The value of this property should be the value returned for the target database by * {@link DatabaseMetaData#getDatabaseMinorVersion()}</li> * </ul> */ @Parameter private Integer databaseMinorVersion; public Integer getDatabaseMinorVersion() { return databaseMinorVersion; } /** * naming strategy that implements {@link org.hibernate.cfg.NamingStrategy} * <p> * this is Hibernate-only option. */ @Parameter private String namingStrategy; public String getNamingStrategy() { return namingStrategy; } /** * dialect class * <p> * use this parameter if you want use custom dialect class. default is detect from JDBC connection or using * {@link #databaseProductName}, {@link #databaseMajorVersion}, and {@link #databaseMinorVersion}. * <p> * this is Hibernate-only option. */ @Parameter private String dialect; public String getDialect() { return dialect; } /** * line separator for generated schema file. * <p> * support value is one of <code>CRLF</code> (windows default), <code>LF</code> (*nix, max osx), and <code>CR</code> * (classic mac), in case-insensitive. * <p> * default value is system property <code>line.separator</code>. if JVM cannot detect <code>line.separator</code>, * then use <code>LF</code> by <a href="http://git-scm.com/book/en/Customizing-Git-Git-Configuration">git * <code>core.autocrlf</code> handling</a>. */ @Parameter private String lineSeparator = System.getProperty("line.separator", "\n"); private static final Map<String, String> LINE_SEPARATOR_MAP = new HashMap<String, String>(); static { LINE_SEPARATOR_MAP.put("CR", "\r"); LINE_SEPARATOR_MAP.put("LF", "\n"); LINE_SEPARATOR_MAP.put("CRLF", "\r\n"); } public String getLineSeparator() { String actual = StringUtils.isEmpty(lineSeparator) ? null : LINE_SEPARATOR_MAP.get(lineSeparator.toUpperCase()); return actual == null ? System.getProperty("line.separator", "\n") : actual; } private static final URL[] EMPTY_URLS = new URL[0]; private ClassLoader getProjectClassLoader() throws MojoExecutionException { try { // compiled classes List<String> classfiles = this.project.getCompileClasspathElements(); if (this.scanTestClasses) { classfiles.addAll(this.project.getTestClasspathElements()); } // classpath to url List<URL> classURLs = new ArrayList<URL>(classfiles.size()); for (String classfile : classfiles) { classURLs.add(new File(classfile).toURI().toURL()); } // dependency artifacts to url ArtifactResolutionRequest sharedreq = new ArtifactResolutionRequest().setResolveRoot(true) .setResolveTransitively(true).setLocalRepository(this.session.getLocalRepository()) .setRemoteRepositories(this.project.getRemoteArtifactRepositories()); ArtifactRepository repository = this.session.getLocalRepository(); Set<Artifact> artifacts = this.project.getDependencyArtifacts(); for (Artifact artifact : artifacts) { if (!Artifact.SCOPE_TEST.equalsIgnoreCase(artifact.getScope())) { ArtifactResolutionRequest request = new ArtifactResolutionRequest(sharedreq) .setArtifact(artifact); ArtifactResolutionResult result = this.resolver.resolve(request); if (result.isSuccess()) { File file = repository.find(artifact).getFile(); if (file != null) { classURLs.add(file.toURI().toURL()); } } } } for (URL url : classURLs) { this.log.info(" * classpath: " + url); } return new URLClassLoader(classURLs.toArray(EMPTY_URLS), this.getClass().getClassLoader()); } catch (Exception e) { this.log.error(e); throw new MojoExecutionException("Error while creating classloader", e); } } private boolean isDatabaseTarget() { return !PersistenceUnitProperties.SCHEMA_GENERATION_NONE_ACTION.equalsIgnoreCase(this.databaseAction); } private boolean isScriptTarget() { return !PersistenceUnitProperties.SCHEMA_GENERATION_NONE_ACTION.equalsIgnoreCase(this.scriptAction); } private void generate() throws Exception { Map<String, Object> map = new HashMap<String, Object>(); /* * Common JPA options */ // mode map.put(PersistenceUnitProperties.SCHEMA_GENERATION_DATABASE_ACTION, this.databaseAction.toLowerCase()); map.put(PersistenceUnitProperties.SCHEMA_GENERATION_SCRIPTS_ACTION, this.scriptAction.toLowerCase()); // output files if (this.isScriptTarget()) { if (this.outputDirectory == null) { throw new NullArgumentException("outputDirectory is required for script generation."); } map.put(PersistenceUnitProperties.SCHEMA_GENERATION_SCRIPTS_CREATE_TARGET, this.getCreateOutputFile().toURI().toString()); map.put(PersistenceUnitProperties.SCHEMA_GENERATION_SCRIPTS_DROP_TARGET, this.getDropOutputFile().toURI().toString()); } // database emulation options map.put(PersistenceUnitProperties.SCHEMA_DATABASE_PRODUCT_NAME, this.databaseProductName); map.put(PersistenceUnitProperties.SCHEMA_DATABASE_MAJOR_VERSION, this.databaseMajorVersion == null ? null : String.valueOf(this.databaseMajorVersion)); map.put(PersistenceUnitProperties.SCHEMA_DATABASE_MINOR_VERSION, this.databaseMinorVersion == null ? null : String.valueOf(this.databaseMinorVersion)); // database options map.put(PersistenceUnitProperties.JDBC_DRIVER, this.jdbcDriver); map.put(PersistenceUnitProperties.JDBC_URL, this.jdbcUrl); map.put(PersistenceUnitProperties.JDBC_USER, this.jdbcUser); map.put(PersistenceUnitProperties.JDBC_PASSWORD, this.jdbcPassword); // source selection map.put(PersistenceUnitProperties.SCHEMA_GENERATION_CREATE_SOURCE, this.createSourceMode); if (this.createSourceFile == null) { if (!PersistenceUnitProperties.SCHEMA_GENERATION_METADATA_SOURCE.equals(this.createSourceMode)) { throw new IllegalArgumentException( "create source file is required for mode " + this.createSourceMode); } } else { map.put(PersistenceUnitProperties.SCHEMA_GENERATION_CREATE_SCRIPT_SOURCE, this.createSourceFile.toURI().toString()); } map.put(PersistenceUnitProperties.SCHEMA_GENERATION_DROP_SOURCE, this.dropSourceMode); if (this.dropSourceFile == null) { if (!PersistenceUnitProperties.SCHEMA_GENERATION_METADATA_SOURCE.equals(this.dropSourceMode)) { throw new IllegalArgumentException("drop source file is required for mode " + this.dropSourceMode); } } else { map.put(PersistenceUnitProperties.SCHEMA_GENERATION_DROP_SCRIPT_SOURCE, this.dropSourceFile.toURI().toString()); } /* * EclipseLink specific */ // persistence.xml map.put(PersistenceUnitProperties.ECLIPSELINK_PERSISTENCE_XML, this.persistenceXml); /* * Hibernate specific */ // naming strategy map.put(AvailableSettings.NAMING_STRATEGY, this.namingStrategy); // auto-detect map.put(AvailableSettings.AUTODETECTION, "class,hbm"); // dialect (without jdbc connection) if (this.dialect == null && this.jdbcUrl == null) { DialectResolutionInfo info = new DialectResolutionInfo() { @Override public String getDriverName() { return null; } @Override public int getDriverMinorVersion() { return 0; } @Override public int getDriverMajorVersion() { return 0; } @Override public String getDatabaseName() { return databaseProductName; } @Override public int getDatabaseMinorVersion() { return databaseMinorVersion; } @Override public int getDatabaseMajorVersion() { return databaseMajorVersion; } }; Dialect detectedDialect = StandardDialectResolver.INSTANCE.resolveDialect(info); this.dialect = detectedDialect.getClass().getName(); } if (this.dialect != null) { map.put(org.hibernate.cfg.AvailableSettings.DIALECT, this.dialect); } if (!this.isDatabaseTarget() && StringUtils.isEmpty(this.jdbcUrl)) { map.put(AvailableSettings.SCHEMA_GEN_CONNECTION, new ConnectionMock(this.getDatabaseProductName(), this.getDatabaseMajorVersion(), this.getDatabaseMinorVersion())); } /* force override JTA to RESOURCE_LOCAL */ map.put(PersistenceUnitProperties.TRANSACTION_TYPE, "RESOURCE_LOCAL"); map.put(PersistenceUnitProperties.JTA_DATASOURCE, null); map.put(PersistenceUnitProperties.NON_JTA_DATASOURCE, null); map.put(PersistenceUnitProperties.VALIDATION_MODE, "NONE"); Persistence.generateSchema(this.persistenceUnitName, map); } private static final Pattern CREATE_DROP_PATTERN = Pattern .compile("((?:create|drop|alter)\\s+(?:table|view|sequence))", Pattern.CASE_INSENSITIVE); private void postProcess() throws IOException { final String linesep = this.getLineSeparator(); List<File> files = Arrays.asList(this.getCreateOutputFile(), this.getDropOutputFile()); for (File file : files) { // check file exists if (file == null || !file.exists()) { continue; } File tempFile = File.createTempFile("script", null, this.getOutputDirectory()); try { // read/write with eol BufferedReader reader = new BufferedReader(new FileReader(file)); PrintWriter writer = new PrintWriter(tempFile); try { String line = null; while ((line = reader.readLine()) != null) { line = CREATE_DROP_PATTERN.matcher(line).replaceAll(";$1"); for (String s : line.split(";")) { if (StringUtils.isBlank(s)) { continue; } s = s.trim(); if (!s.endsWith(";")) { s += ";"; } writer.print(this.isFormat() ? format(s) : s); writer.print(linesep); } } writer.flush(); } finally { reader.close(); writer.close(); } } finally { file.delete(); tempFile.renameTo(file); } } } private static final Pattern PATTERN_CREATE_TABLE = Pattern.compile("(?i)^create(\\s+\\S+)?\\s+(?:table|view)"), PATTERN_CREATE_INDEX = Pattern.compile("(?i)^create(\\s+\\S+)?\\s+index"), PATTERN_ALTER_TABLE = Pattern.compile("(?i)^alter\\s+table"); String format(String s) { final String linesep = this.getLineSeparator(); s = s.replaceAll("^([^(]+\\()", "$1\r\n\t").replaceAll("\\)[^()]*$", "\r\n$0") .replaceAll("((?:[^(),\\s]+|\\S\\([^)]+\\)[^),]*),)\\s*", "$1\r\n\t"); StringBuilder builder = new StringBuilder(); boolean completed = true; if (PATTERN_CREATE_TABLE.matcher(s).find()) { for (String it : s.split("\r\n")) { if (it.matches("^\\S.*$")) { if (!completed) { builder.append(linesep); completed = true; } builder.append(it).append(linesep); } else if (completed) { if (it.matches("^\\s*[^(]+(?:[^(),\\s]+|\\S\\([^)]+\\)[^),]*),\\s*$")) { builder.append(it).append(linesep); } else { builder.append(it); completed = false; } } else { builder.append(it.trim()); if (it.matches("[^)]+\\).*$")) { builder.append(linesep); completed = true; } } } } else if (PATTERN_CREATE_INDEX.matcher(s).find()) { for (String it : s.replaceAll("(?i)^(create(\\s+\\S+)?\\s+index\\s+\\S+)\\s*", "$1\r\n\t") .split("\r\n")) { if (builder.length() == 0) { builder.append(it).append(linesep); } else if (completed) { if (it.matches("^\\s*[^(]+(?:[^(),\\s]+|\\S\\([^)]+\\)[^),]*),\\s*$")) { builder.append(it).append(linesep); } else { builder.append(it); completed = false; } } else { builder.append(it.trim()); if (it.matches("[^)]+\\).*$")) { builder.append(linesep); completed = true; } } } String tmp = builder.toString(); builder.setLength(0); builder.append(tmp.replaceAll("(?i)(asc|desc)\\s*(on)", "$2")); } else if (PATTERN_ALTER_TABLE.matcher(s).find()) { for (String it : s.replaceAll("(?i)^(alter\\s+table\\s+\\S+)\\s*", "$1\r\n\t") .replaceAll("(?i)\\)\\s*(references)", ")\r\n\t$1").split("\r\n")) { if (builder.length() == 0) { builder.append(it).append(linesep); } else if (completed) { if (it.matches("^\\s*[^(]+(?:[^(),\\s]+|\\S\\([^)]+\\)[^),]*),\\s*$")) { builder.append(it).append(linesep); } else { builder.append(it); completed = false; } } else { builder.append(it.trim()); if (it.matches("[^)]+\\).*$")) { builder.append(linesep); completed = true; } } } } else { builder.append(s); } return builder.toString().trim(); } @Override public void execute() throws MojoExecutionException, MojoFailureException { if (this.skip) { log.info("schema generation is skipped."); return; } if (this.outputDirectory != null && !this.outputDirectory.exists()) { this.outputDirectory.mkdirs(); } final ClassLoader classLoader = this.getProjectClassLoader(); // driver load hack // http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-from-an-arbitrary-location if (StringUtils.isNotBlank(this.jdbcDriver)) { try { Driver driver = (Driver) classLoader.loadClass(this.jdbcDriver).newInstance(); DriverManager.registerDriver(driver); } catch (Exception e) { throw new MojoExecutionException("Dependency for driver-class " + this.jdbcDriver + " is missing!", e); } } // generate schema Thread thread = Thread.currentThread(); ClassLoader currentClassLoader = thread.getContextClassLoader(); try { thread.setContextClassLoader(classLoader); this.generate(); } catch (Exception e) { throw new MojoExecutionException("Error while running", e); } finally { thread.setContextClassLoader(currentClassLoader); } // post-process try { this.postProcess(); } catch (IOException e) { throw new MojoExecutionException("Error while post-processing script file", e); } } }