All Downloads are FREE. Search and download functionalities are using the official Maven repository.

au.net.causal.maven.plugins.boxdb.db.H2Database Maven / Gradle / Ivy

package au.net.causal.maven.plugins.boxdb.db;

import au.net.causal.maven.plugins.boxdb.DependencyUtils;
import au.net.causal.maven.plugins.boxdb.ImageCheckerUtils;
import au.net.causal.maven.plugins.boxdb.JavaRunner;
import com.google.common.collect.ImmutableList;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.aether.resolution.DependencyResolutionException;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeoutException;

public class H2Database extends FileBasedDatabase
{
    static final String H2_DATABASE_GROUP_ID = "com.h2database";
    static final String H2_DATABASE_ARTIFACT_ID = "h2";

    private static volatile boolean h2Running;

    public H2Database(BoxConfiguration boxConfiguration,
                      ProjectConfiguration projectConfiguration,
                      BoxContext context)
    {
        super(boxConfiguration, projectConfiguration, context);
    }

    @Override
    public boolean exists()
    throws BoxDatabaseException
    {
        if (!Files.exists(getBoxConfiguration().getDatabaseFile()))
            return false;

        //Also check databaseName exists underneath
        Path databaseDir = getBoxConfiguration().getDatabaseFile().resolve(getBoxConfiguration().getDatabaseName() + ".mv.db");
        return Files.exists(databaseDir);
    }

    @Override
    public boolean isRunning()
    throws BoxDatabaseException
    {
        return h2Running;
    }

    @Override
    public void start()
    throws BoxDatabaseException
    {
        Path dbDir = getBoxConfiguration().getDatabaseFile().toAbsolutePath();
        getContext().getLog().info("H2 home: " + dbDir);

        try
        {
            JavaRunner runner = getContext().createJavaRunner("org.h2.tools.Server", getH2DatabaseDependencies());

            List baseArgs = ImmutableList.of("-tcp", "-tcpPort", String.valueOf(getBoxConfiguration().getDatabasePort()),
                                                     "-baseDir", dbDir.toString());
            List fullArgs = ImmutableList.builder().addAll(baseArgs)
                                                                   .add("-ifNotExists")
                                                                   .build();

            //Multiple attempts to launch, try with -ifNotExists but this isn't needed in older versions
            //so if the first attempt fails launch without that option
            List argSets = ImmutableList.of(fullArgs.toArray(new String[0]),
                                                      baseArgs.toArray(new String[0]));

            InvocationTargetException lastError = null;
            Iterator i = argSets.iterator();
            do
            {
                String[] args = i.next();
                try
                {
                    runner.execute(args);
                    lastError = null;
                }
                catch (InvocationTargetException e)
                {
                    lastError = e;
                    if (!isUnsupportedServerOptionError(e))
                        throw e;

                    //If we know the exception was due to an unsupported arg, continue on
                    getContext().getLog().debug("Got an unsupported arg error so attempting with different args: " + e, e);
                }
            }
            while (i.hasNext() && lastError != null);

            if (lastError != null)
                throw lastError;

            h2Running = true;
        }
        catch (DependencyResolutionException e)
        {
            throw new BoxDatabaseException("Error resolving dependencies for H2: " + e, e);
        }
        catch (ClassNotFoundException | NoSuchMethodException | IOException | InvocationTargetException e)
        {
            throw new BoxDatabaseException("Error executing H2: " + e, e);
        }
    }

    private static boolean isUnsupportedServerOptionError(InvocationTargetException e)
    {
        if (e.getCause() instanceof SQLException)
        {
            SQLException ex = (SQLException)e.getCause();
            if (ex.getErrorCode() == 50100) //ErrorCode.FEATURE_NOT_SUPPORTED_1
                return true;
        }

        return false;
    }

    @Override
    public void stop()
    throws BoxDatabaseException
    {
        try
        {
            JavaRunner runner = getContext().createJavaRunner("org.h2.tools.Server", getH2DatabaseDependencies());
            runner.execute("-tcpShutdown", "tcp://localhost:" + getBoxConfiguration().getDatabasePort());
        }
        catch (InvocationTargetException e)
        {
            //H2 also registers shutdown hook, so if this executes as well we have two shutdown calls,
            //the 2nd failing with SQL error "function STOP_SERVER not found" which we can safely ignore
            if (e.getTargetException() instanceof SQLException)
            {
                SQLException sqlException = (SQLException)e.getTargetException();
                if (sqlException.getErrorCode() == 90022) //Function not found
                {
                    getContext().getLog().debug("Got a SQL error while shutting down H2 database, this is probably OK: " + e.getTargetException(), e);
                    return;
                }
            }
            throw new BoxDatabaseException("Error executing H2: " + e, e);
        }
        catch (DependencyResolutionException e)
        {
            throw new BoxDatabaseException("Error resolving dependencies for H2: " + e, e);
        }
        catch (ClassNotFoundException | NoSuchMethodException | IOException e)
        {
            throw new BoxDatabaseException("Error executing H2: " + e, e);
        }
    }

    @Override
    public void createAndStart()
    throws BoxDatabaseException
    {
        start();
    }

    @Override
    public void delete()
    throws BoxDatabaseException
    {
        getContext().getLog().info("Deleting H2 DB " + getBoxConfiguration().getDatabaseFile());

        try
        {
            FileUtils.deleteDirectory(getBoxConfiguration().getDatabaseFile().toFile());
        }
        catch (IOException e)
        {
            throw new BoxDatabaseException("Failed to delete H2 database in " + getBoxConfiguration().getDatabaseFile() + ": " + e, e);
        }
    }

    @Override
    public void deleteImage()
    throws BoxDatabaseException
    {
        //Not going to wipe H2 from local repo, so do nothing
    }

    @Override
    public JdbcConnectionInfo jdbcConnectionInfo(DatabaseTarget target)
    throws BoxDatabaseException
    {
        String uri = "jdbc:h2:tcp://localhost:" + getBoxConfiguration().getDatabasePort() + "/" + getBoxConfiguration().getDatabaseName();
        return new JdbcConnectionInfo(uri,
                    target.user(getBoxConfiguration()), target.password(getBoxConfiguration()),
                    "localhost", getBoxConfiguration().getDatabasePort());
    }

    @Override
    public JdbcDriverInfo jdbcDriverInfo()
    throws BoxDatabaseException
    {
        return new JdbcDriverInfo(getH2DatabaseDependencies(), "org.h2.Driver");
    }

    @Override
    public void waitUntilStarted(Duration maxTimeToWait)
    throws TimeoutException, BoxDatabaseException
    {
        DatabaseUtils.waitUntilTcpPortResponding(maxTimeToWait, this, getContext());
    }

    @Override
    public void executeScript(URL script, DatabaseTarget targetDatabase, Duration timeout)
    throws IOException, SQLException, BoxDatabaseException
    {
        try (Reader reader = new InputStreamReader(script.openStream(), StandardCharsets.UTF_8))
        {
            executeScript(reader, targetDatabase, timeout);
        }
    }

    protected DataSourceBuilder dataSourceBuilder(DatabaseTarget target)
    throws BoxDatabaseException
    {
        DataSourceBuilder builder = new DataSourceBuilder(getContext())
                                            .dataSourceClassName("org.h2.jdbcx.JdbcDataSource")
                                            .dependencies(jdbcDriverInfo().getDependencies())
                                            .configureDataSource("setUrl", String.class, jdbcConnectionInfo(target).getUri());

        if (target == DatabaseTarget.USER)
        {
            builder.configureDataSource("setUser", String.class, target.user(getBoxConfiguration()));
            builder.configureDataSource("setPassword", String.class, target.password(getBoxConfiguration()));
        }

        return builder;
    }
    
    @Override
    public Connection createJdbcConnection(DatabaseTarget targetDatabase)
    throws SQLException, BoxDatabaseException, IOException
    {
        DataSource dataSource = dataSourceBuilder(targetDatabase).create();
        return dataSource.getConnection();
    }

    @Override
    public void executeScript(Reader scriptReader, DatabaseTarget targetDatabase, Duration timeout)
    throws IOException, SQLException, BoxDatabaseException
    {
        executeJdbcScript(scriptReader, targetDatabase);
    }

    @Override
    public void executeSql(String sql, DatabaseTarget targetDatabase, Duration timeout)
    throws IOException, SQLException, BoxDatabaseException
    {
        try (Reader reader = new StringReader(sql))
        {
            executeScript(reader, targetDatabase, timeout);
        }
    }

    @Override
    public String getName()
    {
        return getBoxConfiguration().getDatabaseName();
    }

    @Override
    public void configureNewDatabase()
    throws IOException, SQLException, BoxDatabaseException
    {
        DataSource dataSource = dataSourceBuilder(DatabaseTarget.ADMIN).create();
        try (Connection con = dataSource.getConnection())
        {
            //This creates the user, now run a command to set up the database user
            try (Statement stat = con.createStatement())
            {
                stat.execute("CREATE USER IF NOT EXISTS " + getBoxConfiguration().getDatabaseUser().toUpperCase(Locale.ENGLISH) + " PASSWORD " + sqlString(getBoxConfiguration().getDatabasePassword()) + " ADMIN");
                
                if (!getBoxConfiguration().getDatabaseUser().equals(getBoxConfiguration().getAdminUser()))
                    stat.execute("CREATE USER IF NOT EXISTS " + getBoxConfiguration().getAdminUser().toUpperCase(Locale.ENGLISH) + " PASSWORD " + sqlString(getBoxConfiguration().getAdminPassword()) + " ADMIN");

                if (!StringUtils.isEmpty(getBoxConfiguration().getDatabaseCollation())) 
                {
                    String[] splitCollation = getBoxConfiguration().getDatabaseCollation().split(";", 2);
                    String locale = splitCollation[0];
                    String collationStatement = "SET DATABASE COLLATION " + locale;
                    if (splitCollation.length > 1)
                    {
                        String collationStrength = splitCollation[1];
                        collationStatement = collationStatement + " STRENGTH " + collationStrength;
                    }
                    
                    stat.execute(collationStatement);
                }
                else
                    stat.execute("SET DATABASE COLLATION OFF");
            }

            if (!StringUtils.isEmpty(getBoxConfiguration().getDatabaseEncoding()) &&
                !getBoxConfiguration().getDatabaseEncoding().equalsIgnoreCase(CommonEncoding.UNICODE.name()))
            {
                throw new BoxDatabaseException("Unsupported database encoding '" + getBoxConfiguration().getDatabaseEncoding() +
                        "', only '" + CommonEncoding.UNICODE.name().toLowerCase(Locale.ENGLISH) + "' is supported.");
            }

            getContext().getLog().info("Created H2 database " + getName());
        }
    }

    private String sqlString(String s)
    {
        StringBuilder buf = new StringBuilder();
        buf.append('\'');
        for (char c : s.toCharArray())
        {
            if (c == '\'')
                buf.append("\'\'");
            else
                buf.append(c);
        }
        buf.append('\'');

        return buf.toString();
    }

    @Override
    public void backup(Path backupFile, BackupFileTypeHint backupFileTypeHint)
    throws BoxDatabaseException, IOException, SQLException
    {
        //Binary backup type can be handled by superclass
        if (backupFileTypeHint == BackupFileTypeHint.COMPACT)
            super.backup(backupFile, backupFileTypeHint);
        else
            executeSql("SCRIPT TO '" + escapeSqlString(backupFile.toAbsolutePath().toString()) + "'", DatabaseTarget.USER, getProjectConfiguration().getBackupTimeout());
    }

    private String escapeSqlString(String s)
    {
        s = s.replace("'", "''");
        return s;
    }

    @Override
    public void restore(URL backupResource)
    throws BoxDatabaseException, IOException, SQLException
    {
        //If it's a full archive backup, then just use superclass
        if (isBackupArchive(backupResource))
        {
            super.restore(backupResource);
            return;
        }

        //Otherwise it was a script backup, so use the database to restore
        executeScript(backupResource, DatabaseTarget.ADMIN, getProjectConfiguration().getBackupTimeout());
    }

    @Override
    public List logFiles() throws BoxDatabaseException, IOException 
    {
        //There might be a trace log file if this was configured when the user opened the DB
        //So we'll use that as the main log file
        Path traceLogFile = getBoxConfiguration().getDatabaseFile().resolve(getBoxConfiguration().getDatabaseName() + ".trace.db");
        
        //Assume log file is written in platform default encoding
        return Collections.singletonList(new FileDatabaseLog(traceLogFile.getFileName().toString(), traceLogFile, Charset.defaultCharset()));
    }

    /**
     * @return H2 database / driver dependencies.
     */
    protected List getH2DatabaseDependencies()
    {
        return Collections.singletonList(new RunnerDependency(H2_DATABASE_GROUP_ID, H2_DATABASE_ARTIFACT_ID,
                                                              getBoxConfiguration().getDatabaseVersion()));
    }

    @Override
    public void prepareImage()
    throws BoxDatabaseException
    {
        try
        {
            DependencyUtils.resolveDependencies(getH2DatabaseDependencies(),
                                                getContext().getRepositorySystem(),
                                                getContext().getRepositorySystemSession(),
                                                getContext().getRemoteRepositories());
        }
        catch (DependencyResolutionException e)
        {
            throw new BoxDatabaseException("Failed to download dependencies for database: " + e.getMessage(), e);
        }
    }

    @Override
    public Collection checkImage()
    throws BoxDatabaseException
    {
        return Collections.singleton(ImageCheckerUtils.checkImageUsingMavenDependencies("H2 database", getContext(), getH2DatabaseDependencies()));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy