de.jpdigital.maven.plugins.hibernate4ddl.GenerateDdlMojo.java Source code

Java tutorial

Introduction

Here is the source code for de.jpdigital.maven.plugins.hibernate4ddl.GenerateDdlMojo.java

Source

/*
 * Copyright (C) 2014 Jens Pelzetter <jens@jp-digital.de>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.jpdigital.maven.plugins.hibernate4ddl;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;

import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.hibernate.cfg.Configuration;
import org.hibernate.envers.tools.hbm2ddl.EnversSchemaGenerator;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * Goal which creates DDL SQL files for the JPA entities in the project (using
 * the Hibernate 4 SchemaExport class}.
 *
 * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
 */
@Mojo(name = "gen-ddl", defaultPhase = LifecyclePhase.PROCESS_CLASSES, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, threadSafe = true)
public class GenerateDdlMojo extends AbstractMojo {

    /**
     * Location of the output file.
     */
    @Parameter(defaultValue = "src/main/sql/ddl/", property = "outputDir", required = true)
    private File outputDirectory;

    /**
     * Packages containing the entity files for which the SQL DDL scripts shall
     * be generated.
     */
    @Parameter(required = true)
    private String[] packages;

    /**
     * Database dialects for which create scripts shall be generated. For
     * available dialects refer to the documentation the {@link Dialect}
     * enumeration.
     */
    @Parameter(required = true)
    private String[] dialects;

    /**
     * Set this to <code>true</code> if you use the Envers feature of Hibernate.
     * When set to <code>true</code> the {@code SchemaExport} implementation for
     * Envers is used. This is necessary to create the additional tables
     * required by Envers. Default value is {@code false}.
     */
    @Parameter(required = false)
    private boolean useEnvers;

    /**
     * The {@code persistence.xml} file to use to read properties etc. Default
     * value is {@code src/main/resources/META-INF/persistence.xml}. If the file
     * is not present it is ignored. If the file is present all properties set
     * using a {@code <property>} element are set on the Hibernate
     * configuration.
     */
    @Parameter(defaultValue = "${basedir}/src/main/resources/META-INF/persistence.xml", required = false)
    private File persistenceXml;

    @Component
    private transient MavenProject project;

    /**
     * The Mojo's execute method.
     *
     * @throws MojoExecutionException if the Mojo can't be executed.
     * @throws MojoFailureException   if something goes wrong while to Mojo is
     *                                executed.
     */
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        final File outputDir = outputDirectory;

        getLog().info(String.format("Generating DDL SQL files in %s.", outputDir.getAbsolutePath()));

        //Check if the output directory exists.
        if (!outputDir.exists()) {
            final boolean result = outputDir.mkdirs();
            if (!result) {
                throw new MojoFailureException("Failed to create output directory for SQL DDL files.");
            }
        }

        //Read the dialects from the parameter and convert them to instances of the dialect enum.
        final Set<Dialect> dialectsList = new HashSet<>();
        for (final String dialect : dialects) {
            convertDialect(dialect, dialectsList);
        }

        //Find the entity classes in the packages.
        final Set<Class<?>> entityClasses = new HashSet<>();
        for (final String packageName : packages) {
            final Set<Class<?>> packageEntities = EntityFinder.forPackage(project, getLog(), packageName)
                    .findEntities();
            entityClasses.addAll(packageEntities);

            //findEntitiesForPackage(packageName, entityClasses);
        }
        getLog().info(String.format("Found %d entities.", entityClasses.size()));

        //Generate the SQL scripts
        for (final Dialect dialect : dialectsList) {
            generateDdl(dialect, entityClasses);
        }
    }

    public File getOutputDirectory() {
        return outputDirectory;
    }

    public void setOutputDirectory(final File outputDirectory) {
        this.outputDirectory = outputDirectory;
    }

    public String[] getPackages() {
        return Arrays.copyOf(packages, packages.length);
    }

    public void setPackages(final String... packages) {
        this.packages = Arrays.copyOf(packages, packages.length);
    }

    public String[] getDialects() {
        return Arrays.copyOf(dialects, dialects.length);
    }

    public void setDialects(final String... dialects) {
        this.dialects = Arrays.copyOf(dialects, dialects.length);
    }

    public boolean isUseEnvers() {
        return useEnvers;
    }

    public void setUseEnvers(final boolean useEnvers) {
        this.useEnvers = useEnvers;
    }

    public File getPersistenceXml() {
        return persistenceXml;
    }

    public void setPersistenceXml(final File persistenceXml) {
        this.persistenceXml = persistenceXml;
    }

    /**
     * Helper method for converting the dialects from {@code String} to
     * instances of the {@link Dialect} enumeration.
     *
     * @param dialect      The dialect to convert.
     * @param dialectsList The lists of dialects where the converted dialect is
     *                     stored.
     *
     * @throws MojoFailureException If the dialect string could not be
     *                              converted, for example if it is misspelled.
     *                              This will cause a {@code Build Failure}
     */
    private void convertDialect(final String dialect, final Set<Dialect> dialectsList) throws MojoFailureException {

        try {
            dialectsList.add(Dialect.valueOf(dialect.toUpperCase(Locale.ENGLISH)));
        } catch (IllegalArgumentException ex) {
            final StringBuffer buffer = new StringBuffer();
            for (final Dialect avilable : Dialect.values()) {
                buffer.append(avilable.toString()).append('\n');
            }

            throw new MojoFailureException(
                    String.format("Can't convert the configured dialect '%s' to a dialect classname. "
                            + "Available dialects are:%n" + "%s", dialect, buffer.toString()),
                    ex);
        }
    }

    /**
     * Helper method for generating the DDL classes for a specific dialect. This
     * is place for the real work is done. The method first creates an instance
     * of the {@link Configuration} class from Hibernate an puts the appropriate
     * values into it. It then creates an instance of the {@link SchemaExport}
     * class from the Hibernate API, configured this class, for example by
     * setting {@code format} to {@code true} so that the generated SQL files
     * are formatted nicely. After that it calls the
     * {@link SchemaExport#execute(boolean, boolean, boolean, boolean)} method
     * which will create the SQL script file. The method is called in a way
     * which requires <em>no</em> database connection.
     *
     *
     * @param dialect       The dialect for which the DDL files is generated.
     * @param entityClasses The entity classes for which the DDL file is
     *                      generated.
     *
     * @throws MojoFailureException if something goes wrong.
     */
    private void generateDdl(final Dialect dialect, final Set<Class<?>> entityClasses) throws MojoFailureException {
        final Configuration configuration = new Configuration();

        processPersistenceXml(configuration);

        configuration.setProperty("hibernate.hbm2ddl.auto", "create");

        for (final Class<?> entityClass : entityClasses) {
            configuration.addAnnotatedClass(entityClass);
        }

        configuration.setProperty("hibernate.dialect", dialect.getDialectClass());

        final SchemaExport export;
        if (useEnvers) {
            export = new EnversSchemaGenerator(configuration).export();
        } else {
            export = new SchemaExport(configuration);

        }
        export.setDelimiter(";");

        final Path tmpDir;
        try {
            tmpDir = Files.createTempDirectory("maven-hibernate-ddl-plugin");
        } catch (IOException ex) {
            throw new MojoFailureException("Failed to create work dir.", ex);
        }
        export.setOutputFile(
                String.format("%s/%s.sql", tmpDir.toString(), dialect.name().toLowerCase(Locale.ENGLISH)));
        export.setFormat(true);
        export.execute(true, false, false, true);

        writeOutputFile(dialect, tmpDir);
    }

    private void processPersistenceXml(final Configuration configuration) {
        if (persistenceXml != null) {
            getLog().info("persistence.xml available, locking for properties...");

            try (InputStream inStream = new FileInputStream(persistenceXml)) {
                final SAXParser parser;

                parser = SAXParserFactory.newInstance().newSAXParser();

                parser.parse(inStream, new PersistenceXmlHandler(configuration));
            } catch (IOException ex) {
                getLog().error("Failed to open persistence.xml. Not processing properties.", ex);
            } catch (ParserConfigurationException | SAXException ex) {
                getLog().error("Error parsing persistence.xml. Not processing properties", ex);
            }
        }
    }

    private class PersistenceXmlHandler extends DefaultHandler {

        private final transient Configuration configuration;

        public PersistenceXmlHandler(final Configuration configuration) {
            this.configuration = configuration;
        }

        @Override
        public void startElement(final String uri, final String localName, final String qName,
                final Attributes attributes) {
            getLog().info(String.format("Found element with uri = '%s', localName = '%s', qName = '%s'...", uri,
                    localName, qName));

            if ("property".equals(qName)) {
                final String propertyName = attributes.getValue("name");
                final String propertyValue = attributes.getValue("value");

                if (propertyName != null && !propertyName.isEmpty() && propertyValue != null
                        && !propertyValue.isEmpty()) {
                    getLog().info(String.format("Found property %s = %s in persistence.xml", propertyName,
                            propertyValue));
                    configuration.setProperty(propertyName, propertyValue);
                }
            }
        }

    }

    /**
     * Helper method for writing the output files if necessary. The
     * {@link #generateDdl(Dialect, Set)} method writes the output to temporary
     * files. This method checks of the output files have changed and copies the
     * files if necessary.
     */
    private void writeOutputFile(final Dialect dialect, final Path tmpDir) throws MojoFailureException {
        //        final Path outputDir = outputDirectory.toPath();
        //        if (Files.exists(outputDir)) {
        //            if (!Files.isDirectory(outputDir)) {
        //                throw new MojoFailureException("A file with the name of the "
        //                                                   + "output directory already "
        //                                                   + "exists but is not a "
        //                                                   + "directory.");
        //            }
        //        } else {
        //            try {
        //                Files.createDirectory(outputDir);
        //            } catch (IOException ex) {
        //                throw new MojoFailureException(
        //                    String.format("Failed to create the output directory: %s",
        //                                  ex.getMessage()),
        //                    ex);
        //            }
        //        }

        createOutputDir();

        //        final String dirPath;
        //        if (outputDirectory.getAbsolutePath().endsWith("/")) {
        //            dirPath = outputDirectory.getAbsolutePath().substring(
        //                0, outputDirectory.getAbsolutePath().length());
        //        } else {
        //            dirPath = outputDirectory.getAbsolutePath();
        //        }
        //
        //        final Path outputFilePath = Paths.get(String.format(
        //            "%s/%s.sql", dirPath, dialect.name().toLowerCase(Locale.ENGLISH)));
        final Path outputFilePath = createOutputFilePath(dialect);
        final Path tmpFilePath = Paths
                .get(String.format("%s/%s.sql", tmpDir.toString(), dialect.name().toLowerCase(Locale.ENGLISH)));

        if (Files.exists(outputFilePath)) {

            final String outputFileData;
            final String tmpFileData;
            try {
                outputFileData = new String(Files.readAllBytes(outputFilePath), Charset.forName("UTF-8"));
                tmpFileData = new String(Files.readAllBytes(tmpFilePath), Charset.forName("UTF-8"));
            } catch (IOException ex) {
                throw new MojoFailureException(
                        String.format("Failed to check if DDL file content has " + "changed: %s", ex.getMessage()),
                        ex);
            }

            try {
                if (!tmpFileData.equals(outputFileData)) {
                    Files.deleteIfExists(outputFilePath);
                    Files.copy(tmpFilePath, outputFilePath);
                }
            } catch (IOException ex) {
                throw new MojoFailureException(
                        String.format("Failed to copy DDL file content from tmp " + "file to output file: %s",
                                ex.getMessage()),
                        ex);
            }
        } else {
            try {
                Files.copy(tmpFilePath, outputFilePath);
            } catch (IOException ex) {
                throw new MojoFailureException(
                        String.format("Failed to copy tmp file to output file: %s", ex.getMessage()), ex);
            }
        }
    }

    /**
     * Helper for creating the output directory if it does not exist.
     *
     * @return A {@link Path} object describing the output directory.
     *
     * @throws MojoFailureException If The creation of the output directory
     *                              fails.
     */
    private void createOutputDir() throws MojoFailureException {
        final Path outputDir = outputDirectory.toPath();
        if (Files.exists(outputDir)) {
            if (!Files.isDirectory(outputDir)) {
                throw new MojoFailureException("A file with the name of the " + "output directory already "
                        + "exists but is not a " + "directory.");
            }
        } else {
            try {
                Files.createDirectory(outputDir);
            } catch (IOException ex) {
                throw new MojoFailureException(
                        String.format("Failed to create the output directory: %s", ex.getMessage()), ex);
            }
        }
    }

    private Path createOutputFilePath(final Dialect dialect) {
        final String dirPath;
        if (outputDirectory.getAbsolutePath().endsWith("/")) {
            dirPath = outputDirectory.getAbsolutePath().substring(0, outputDirectory.getAbsolutePath().length());
        } else {
            dirPath = outputDirectory.getAbsolutePath();
        }

        return Paths.get(String.format("%s/%s.sql", dirPath, dialect.name().toLowerCase(Locale.ENGLISH)));
    }
}