/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2010-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2010-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library 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 * Lesser General Public License for more details. */ package org.geotoolkit.internal.sql; import java.io.File; import java.io.InputStream; import java.io.IOException; import java.io.FileNotFoundException; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.SQLException; import org.opengis.util.FactoryException; import org.apache.sis.util.CharSequences; import org.apache.sis.util.StringBuilders; import org.geotoolkit.internal.io.Host; import org.geotoolkit.coverage.sql.CoverageDatabase; import org.apache.sis.referencing.factory.sql.EPSGFactory; /** * Runs the Coverage database installation scripts on a given database. * This runner assumes that the file encoding is {@code "UTF-8"}. * <p> * Callers should set the public fields declared in this class before to * invoke {@link #install()}. * * @author Martin Desruisseaux (Geomatys) * @version 3.16 * * @since 3.11 * @module */ public class CoverageDatabaseInstaller extends ScriptRunner { /** * The default login used for read/write operations. */ public static final String ADMINISTRATOR = "geoadmin"; /** * The default login used for read only operations. */ public static final String USER = "geouser"; /** * The default coverages schema. */ public static final String SCHEMA = "coverages"; /** * The default metadata schema. * * @since 3.14 */ public static final String METADATA_SCHEMA = "metadata"; /** * The enums created by the SQL scripts. They will need to be erased from the SQL * scripts before {@linkplain #execute execution} if the database doesn't support * enums. */ private static final String[] ENUMS = { "\"PackMode\"", "\"MI_TransferFunctionTypeCode\"" }; /** * Whatever enums are supported. Enums are not a standard feature; * consequently they are supported only for a few specific databases. * * @since 3.14 */ private final boolean supportsEnumType; /** * {@code true} if the {@code CREATE TRUSTED PROCEDURAL LANGUAGE 'plpgsql'} * instruction needs to be executed. * * @since 3.16 */ private final boolean needsCreateLanguage; /** * {@code true} if the "geoadmin" and "geouser" roles should be created. */ public boolean createRoles; /** * {@code true} if the EPSG database should be copied. */ public boolean createEPSG; /** * The directory of PostGIS installation scripts, or {@code null} if none. */ public File postgisDir; /** * The coverages schema. */ public String schema; /** * The name of the administrator role. This role should have read and write access. */ public String admin; /** * The name of the user role. This role should have only read access. */ public String user; /** * The runner under execution, or {@code null} for this runner. Used in order * to get information about the SQL instruction that failed. */ private transient ScriptRunner runner; /** * Creates a new runner which will execute the statements using the given connection. * * @param connection The connection to the database. * @throws SQLException If an error occurred while executing a SQL statement. */ public CoverageDatabaseInstaller(final Connection connection) throws SQLException { super(connection); if (dialect != Dialect.POSTGRESQL) { throw new UnsupportedOperationException(dialect.toString()); } final DatabaseMetaData metadata = connection.getMetaData(); needsCreateLanguage = dialect.needsCreateLanguage(metadata); supportsEnumType = dialect.supportsEnumType(metadata); setEncoding("UTF-8"); } /** * Invoked after each step has been performed. The values range from 0 to 100 inclusive. * This is only a very approximative information. The default implementation does nothing. * * @param percent The progress as a value from 0 to 100 inclusive. * @param schema The name of the schema about to be created, or {@code null}. */ protected void progress(int percent, String schema) { } /** * If the given value is null or empty, returns the default value. * Otherwise if the given value is different than the default value, quote it. */ private static String ensureNonNull(String value, String defaultValue, final String quote) { if (value == null || (value = value.trim()).isEmpty()) { value = defaultValue; } else if (!value.equals(defaultValue)) { value = quote + value + quote; } return value; } /** * Starts the installation. In case of failure, use {@link #toString()} for information * about the line which caused the error. * * @return The number of rows added or modified as a result of the script execution. * @throws IOException If an error occurred while reading the input. * @throws SQLException If an error occurred while executing a SQL statement. * @throws FactoryException If an error occurred during the installation of the EPSG database. */ public int install() throws IOException, SQLException, FactoryException { final DatabaseMetaData md = getConnection().getMetaData(); final String quote = md.getIdentifierQuoteString(); user = ensureNonNull(user, USER, quote); admin = ensureNonNull(admin, ADMINISTRATOR, quote); schema = ensureNonNull(schema, SCHEMA, quote); /* * Creates users and language. */ progress(0, null); int n = runFile("prepare.sql"); /* * Creates the postgis schema. */ if (postgisDir != null) { progress(5, PostgisInstaller.DEFAULT_SCHEMA); final PostgisInstaller postgis = new PostgisInstaller(getConnection()); runner = postgis; n += postgis.run(postgisDir); postgis.close(false); // Close the statement, not the connection. progress(30, PostgisInstaller.DEFAULT_SCHEMA); n += runFile("postgis-update.sql"); runner = null; } /* * Creates the epsg schema. */ if (createEPSG) { progress(40, "epsg"); final EPSGFactory installer = new EPSGFactory(null); // TODO: specify the DataSource. installer.install(getConnection()); } /* * Creates the metadata schema. */ progress(75, METADATA_SCHEMA); n += runFile("metadata-create.sql"); /* * Creates the coverages schema. */ progress(80, SCHEMA); n += runFile("coverages-create.sql"); if (dialect == Dialect.POSTGRESQL) { String database = new Host(md.getURL()).path; if (database != null) { n = run("ALTER DATABASE " + quote + database + quote + " SET search_path=public, " + schema + ", " + METADATA_SCHEMA + ", " + PostgisInstaller.DEFAULT_SCHEMA + END_OF_STATEMENT + '\n' + "COMMENT ON DATABASE " + quote + database + quote + " IS 'Geotoolkit.org source of coverages.'" + END_OF_STATEMENT); } } progress(100, null); return n; } /** * Runs the given resource file. * * @param file The resource file to run. * @return The number of rows added or modified as a result of the script execution. * @throws IOException If an error occurred while reading the input. * @throws SQLException If an error occurred while executing a SQL statement. */ private int runFile(final String file) throws IOException, SQLException { runner = null; final InputStream in = CoverageDatabase.class.getResourceAsStream(file); if (in == null) { throw new FileNotFoundException(file); } return run(in); // The stream will be closed by the run method. } /** * Executes the given SQL statement. This method may modify the SQL script if enums, roles * or languages should not be created. Note that this method is invoked only for the scripts * provided in the {@code geotk-coverage-sql} package; the PostGIS and EPSG scripts use their * one installer and consequently are unaffected by the changes performed by this method. * * @param sql The SQL statement to execute. * @return The number of rows added or modified as a result of the statement execution. * @throws SQLException If an error occurred while executing the SQL statement. * @throws IOException If an I/O operation was required and failed. */ @Override protected int execute(final StringBuilder sql) throws SQLException, IOException { final CreateStatementType create = CreateStatementType.fromSQL(sql); if (create != null) switch (create) { case ROLE: if (!createRoles) return 0; else break; case LANGUAGE: if (!needsCreateLanguage) return 0; else break; case CAST: // Fall through case ENUM: if (!supportsEnumType) return 0; else break; case TABLE: { if (!supportsEnumType) { for (final String e : ENUMS) { StringBuilders.replace(sql, e, "varchar"); } } break; } } /* * We can't add comments on enum, since they were not created. */ if (!supportsEnumType && CharSequences.regionMatches(sql, 0, "COMMENT ON TYPE")) { return 0; } StringBuilders.replace(sql, USER, user); StringBuilders.replace(sql, ADMINISTRATOR, admin); StringBuilders.replace(sql, SCHEMA, schema); return super.execute(sql); } /** * Returns the current position (current file and current line in that file). The main purpose * of this method is to provides informations on the position where an exception occurred. */ @Override public String getCurrentPosition() { if (runner != null) { return runner.getCurrentPosition(); } return super.getCurrentPosition(); } /** * Returns a string representation of this runner for debugging purpose. This method * may be invoked after a {@link SQLException} occurred in order to determine the line * in the SQL script that caused the error. */ @Override public String toString() { if (runner != null) { return runner.toString(); } return super.toString(); } }