Package org.geotools.gce.imagemosaic.jdbc.custom

Source Code of org.geotools.gce.imagemosaic.jdbc.custom.JDBCPGRasterConfigurationBuilder

/*
*    GeoTools - The Open Source Java GIS Toolkit
*    http://geotools.org
*
*    (C) 2013, Open Source Geospatial Foundation (OSGeo)
*
*    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.geotools.gce.imagemosaic.jdbc.custom;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.ExecTask;
import org.apache.tools.ant.types.Environment.Variable;
import org.geotools.factory.Hints;
import org.geotools.gce.imagemosaic.jdbc.Config;
import org.geotools.gce.imagemosaic.jdbc.DBDialect;
import org.geotools.gce.imagemosaic.jdbc.ImageMosaicJDBCReader;
import org.geotools.referencing.factory.gridshift.DataUtilities;
import org.geotools.util.Utilities;
import org.geotools.util.logging.Logging;

/**
* A JDBC PGRaster configuration builder class.
* It allows to configure a PGRaster based ImageMosaicJDBC starting from:
*  - a folder containing several tiles previously created through gdal_retile
*  - a configuration bean specifying PG credentials, database, table, coverage name and files extensions
*
* The configuration may be specified in 2 different ways:
* 1) through an input string with the following format:
*     pgraster://USER:PASS@HOST:PORT:DATABASE.SCHEMA.TABLE@EPSGCODE:*.FILE_EXTENSION?OPTIONS#/PATH/TO/RASTER_TILES/"
*    
*     In this case, a JDBCPGRasterConfigurationBean instance will be created on top of this String.
*     That String also contains the RASTER_TILES PATH representing the folder containing tiles previously
*     created with gdal_retile. Note that gdal_retile will create tiles using that structure:
*     - level 0 is put into the target dir
*     - level i is put into subfolder i inside target dir
*    
*     NOTE that -useDirForEachRow gdal option isn't currently supported by the importer.
*    
*     FILE_EXTENSION and OPTIONS configuration properties are optional (in case the user did the tiles import
*     on his own)
*
* 2) a JDBCPGRasterConfigurationBean instance.
*    The input string simply contains the RASTER_TILES PATH on this case.
*
* @author Daniele Romagnoli, GeoSolutions SAS
*/
public class JDBCPGRasterConfigurationBuilder {

    private final static String RASTER2PGSQL_PATH_KEY = "RASTER2PGSQL_PATH";

    /**
     * A configuration builder constructor
     * @param configBean the configuration bean
     * @param configDir the directory containing data to be configured
     */
    public JDBCPGRasterConfigurationBuilder(JDBCPGrasterConfigurationBean configBean, URL configDir) {
        this.configBean = configBean;
        this.configDir = configDir;
    }

    // Simple constants KEYs
    private final static String MOSAIC_KEY = "$MASTER_TABLE";

    private final static String COVERAGE_KEY = "$COVERAGE_NAME";

    private final static String OPTIONS_KEY = "$OPTIONS";

    private final static String SCHEMA_KEY = "$SCHEMA";

    private final static String EPSG_CODE_KEY = "$EPSG_CODE";

    private final static String TABLE_PREFIX_KEY = "$TABLE_PREFIX";

    private final static String SQL_FILE_KEY = "$SQL_FILE";

    private final static String DATABASE_KEY = "$DATABASE";

    private final static String PGUSER_KEY = "$USER";

    private final static String PASSWORD_KEY = "$PASSWORD";

    private final static String PORT_KEY = "$PORT";

    private final static String HOST_KEY = "$HOST";

    private final static String FILES_KEY = "$FILES";

    private final static String LINE_SEPARATOR = System.getProperty("line.separator");

    private final static String DEFAULT_OPTIONS = "-t 128x128";

    private final static String TABLE_CREATION_SQL = "create table "
            + MOSAIC_KEY
            + " (NAME varchar(254) not null,"
            + "TileTable varchar(254)not null, minX FLOAT8, minY FLOAT8, maxX FLOAT8, maxY FLOAT8, resX FLOAT8, resY FLOAT8,"
            + "primary key (NAME,TileTable))";

    private final static String TABLE_CHECK_SQL = "SELECT tiletable FROM " + MOSAIC_KEY;

    private final static String TILE_TABLE_CHECK_SQL = "SELECT COUNT(rid) FROM " + MOSAIC_KEY;

    private final static String TILETABLE_INSERTION_SQL = "insert into " + MOSAIC_KEY
            + " (NAME,TileTable) values (?,?)";

    private final static Logger LOGGER = Logging.getLogger(ImageMosaicJDBCReader.class.getPackage()
            .getName());

    private final static String TEMPLATE_FILE_NAME = "coverage.pgraster.template.xml";

    private static String RASTER2PGSQL_COMMAND = "raster2pgsql";

    private static String EXECUTE = "execute";

    private final static int DEFAULT_EPSG_CODE = 4326;

    private static String IMPORT_COMMAND;

    private static String TEMPLATE;

    private static String PATH;

    private static String RASTER2PGSQL_PATH;

    private static boolean available;

    private static boolean init = false;

    private JDBCPGrasterConfigurationBean configBean;

    private URL configDir;

    static {
        initTemplate();
        checkRaster2Pgsql();
    }

    /**
     * Check whether the raster2pgsql script is available
     * @return
     */
    public static boolean isRaster2PgsqlAvailable() {
        checkRaster2Pgsql();
        return available;
    }

    /**
     * Check the availability of raster2pgsql command
     */
    public static void checkRaster2Pgsql() {
        if (init == false) {
            synchronized (LOGGER) {
                if (init) {
                    return;
                }
                PATH = System.getenv("PATH");
                RASTER2PGSQL_PATH = System.getProperty(RASTER2PGSQL_PATH_KEY);
                File file = null;
                try {
                    // Executing a raster2pgsql invokation to check for its availability
                    String OS = System.getProperty("os.name").toLowerCase();
                    final File dir;
                    if (OS.indexOf("win") >= 0) {
                        EXECUTE = "execute.bat";
                        String executable = RASTER2PGSQL_COMMAND;
                        final String executablePath = RASTER2PGSQL_PATH + File.separatorChar;
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.fine("RASTER2PGSQL_PATH = " + RASTER2PGSQL_PATH);
                        }
                        dir = new File(executablePath);
                        executable =  executablePath + executable + ".exe";
                        RASTER2PGSQL_COMMAND = executable;
                    } else {
                        dir = new File(".");
                    }
                    IMPORT_COMMAND = RASTER2PGSQL_COMMAND + " " + OPTIONS_KEY + " -F " + FILES_KEY + " " + SCHEMA_KEY + "."
                    + TABLE_PREFIX_KEY + " > " + SQL_FILE_KEY + LINE_SEPARATOR + "psql -d " + DATABASE_KEY + " -U " + PGUSER_KEY + " -h " + HOST_KEY + " -p " + PORT_KEY + " -f " + SQL_FILE_KEY;
                    final ExecTask task = new ExecTask();
                    task.setExecutable(RASTER2PGSQL_COMMAND);
                    task.setDir(dir);
                    task.createArg().setValue("-G");
                    Variable variable = new Variable();
                    variable.setKey("PATH");
                    variable.setValue(PATH);
                    task.addEnv(variable);

                    // Logging to a temporary file to avoid logging on system out 
                    file = File.createTempFile("r2pg", ".tmp");
                    task.setOutput(file);
                    task.execute();
                    if (LOGGER.isLoggable(Level.FINE)) {
                        LOGGER.fine("raster2pgsql script is available");
                    }
                    available = true;
                } catch (BuildException e) {
                    if (LOGGER.isLoggable(Level.WARNING)) {
                        LOGGER.warning("Failed to invoke the raster2pgsql script. This is not a problem "
                                + "unless you need to use the raster2pgsql script to automatically configure pgrasters.\n"
                                + e.toString());
                    }
                    available = false;
                } catch (IOException e) {
                    if (LOGGER.isLoggable(Level.WARNING)) {
                        LOGGER.warning("Failed to invoke the raster2pgsql script. This is not a problem "
                                + "unless you need to use the raster2pgsql script to automatically configure pgrasters.\n"
                                + e.toString());
                    }
                    available = false;
                } finally {
                    init = true;
                    if (file != null) {
                        file.delete();
                    }
                }
            }
        }
    }

    /**
     * Initialize the string template parsing the available resource
     */
    private static void initTemplate() {
        InputStream stream = null;
        stream = JDBCPGRasterConfigurationBuilder.class.getResourceAsStream(TEMPLATE_FILE_NAME);

        InputStreamReader streamReader = null;
        BufferedReader reader = null;

        try {

            // Replace with some dedicate method which setup string from resources.
            streamReader = new InputStreamReader(stream);
            reader = new BufferedReader(streamReader);
            String line = null;
            StringBuilder stringBuilder = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                stringBuilder.append(line);
            }

            TEMPLATE = stringBuilder.toString();

        } catch (FileNotFoundException e) {
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.warning(e.getLocalizedMessage());
            }
        } catch (IOException e) {
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.warning(e.getLocalizedMessage());
            }
        } finally {

            // Release resources
            if (reader != null) {
                try {
                    reader.close();
                } catch (Exception ignored) {
                }
            }
            if (streamReader != null) {
                try {
                    streamReader.close();
                } catch (Exception ignored) {
                }
            }
            if (stream != null) {
                try {
                    stream.close();
                } catch (Exception ignored) {
                }
            }
        }
    }

    /**
     * Prepare the PGRaster configuration steps:
     *  - importing tiles into the DB and create metadata table if needed
     *  - prepare the URL of the XML configuration file and return that URL.
     *
     * The input url String may represent:
     * - 1) the path of the folder containing tiles to be mosaicked, previously generated with gdal_retile.
     *   2) an inline configuration specification in that form:
     *      pgraster://USER:PASS@HOST:PORT:DATABASE.SCHEMA.TABLE@EPSGCODE:*.FILE_EXTENSION?OPTIONS#/PATH/TO/RASTER_TILES/"
     *
     * @param url a String referring to the mosaic to be configured.
     * @param hints hints containing a {@link JDBCPGrasterConfigurationBean} bean in case the url string
     * doesn't specify the configuration params inline.
     *
     * @return the URL of the generated XML file containing the ImageMosaicJDBC configuration
     * (mapping + connection + coverage configs)
     */
    public static URL createConfiguration (final String url, final Hints hints) {
        Utilities.ensureNonNull("url", url);
        try {
            JDBCPGRasterConfigurationBuilder builder = null;
            JDBCPGrasterConfigurationBean config = null;
            // Fail in case no raster2pgsql script is available
            if (url.startsWith("pgraster")) {
                // Parse the string containing configuration inline
                config = parseConfig(url);
                final int fileURLIndex = url.indexOf("#");
                final String dataUrl = url.substring(fileURLIndex + 1);
                final String fileUrl = dataUrl.startsWith("file:/") ? dataUrl : "file://" + dataUrl;
                builder = new JDBCPGRasterConfigurationBuilder(config, new URL(fileUrl));
            } else {
                if (hints != null && hints.containsKey(JDBCPGrasterConfigurationBean.CONFIG_KEY)) {
                    // Get the configuration from hints
                    Object object = hints.get(JDBCPGrasterConfigurationBean.CONFIG_KEY);
                    if (object != null && object instanceof JDBCPGrasterConfigurationBean) {
                        config = (JDBCPGrasterConfigurationBean) object;
                        builder = new JDBCPGRasterConfigurationBuilder(config, new URL(url));
                    }
                }
            }
            if (builder != null) {
                if (!isRaster2PgsqlAvailable() && config != null && (config.getFileExtension() != null || config.getImportOptions() != null)) {
                    if (LOGGER.isLoggable(Level.WARNING)) {
                        LOGGER.warning("The specified URL refers to a pgraster but raster2pgsql script is unavailable.\n" +
                                 "Automatic configuration won't be performed. In case raster tiles have been manually imported, " +
                                 "make sure to leave fileExtensions and importOptions parameters empty and repeat the coverage configuration.");
                        return null;
                    }
                }

               
                return builder.buildConfiguration();
            }
            return new URL(url);
        } catch (MalformedURLException mfe) {
            throw new IllegalArgumentException(mfe);
        }
    }

    /**
     * Extract configuration parameters from the provided string.
     * See {@link JDBCPGRasterConfigurationBuilder#createConfiguration(String, Hints)} for the syntax
     * of this string 
     *
     * @param pgrasterUrl
     * @return
     */
    private static JDBCPGrasterConfigurationBean parseConfig(final String pgrasterUrl) {
        if (pgrasterUrl != null && pgrasterUrl.startsWith("pgraster:/")) {
            final int fileURLIndex = pgrasterUrl.indexOf("#");
            if (fileURLIndex < 0) {
                throw new IllegalArgumentException("The specified URL doesn't contain the data folder");
            }

            // Not sure why the GeoserverDataDirectory.findDataFile eats a "/" char. 10 should be 11
            final int prefix = pgrasterUrl.startsWith("pgraster://") ? 11 : 10;

            // Parsing pgUser
            final int pguserEndIndex = pgrasterUrl.indexOf(":", prefix);
            final String pguser = pgrasterUrl.substring(prefix, pguserEndIndex);

            // Parsing password
            final int passwordEndIndex = pgrasterUrl.indexOf("@");
            final String password = pgrasterUrl.substring(pguserEndIndex + 1, passwordEndIndex);

            // Parsing host
            final int hostEndIndex = pgrasterUrl.indexOf(":", passwordEndIndex + 1);
            final String host = pgrasterUrl.substring(passwordEndIndex + 1, hostEndIndex);

            // Parsing port
            final int portEndIndex = pgrasterUrl.indexOf(":", hostEndIndex + 1);
            final String port = pgrasterUrl.substring(hostEndIndex + 1, portEndIndex);

            // Parsing Database
            final int dbEndIndex = pgrasterUrl.indexOf(".", portEndIndex + 1);
            final String db = pgrasterUrl.substring(portEndIndex + 1, dbEndIndex);

            // Parsing schema
            final int schemaEndIndex = pgrasterUrl.indexOf(".", dbEndIndex + 1);
            final String schema = pgrasterUrl.substring(dbEndIndex + 1, schemaEndIndex);

            // Parsing table
            final int tableEndIndex = pgrasterUrl.indexOf(":", schemaEndIndex + 1);
           
            // Parsing EPSGCode
            int epsgCode = DEFAULT_EPSG_CODE;
            final int epsgStartIndex = pgrasterUrl.indexOf("@", schemaEndIndex + 1);
            if (epsgStartIndex != -1) {
                try {
                    epsgCode = Integer.parseInt(pgrasterUrl.substring(epsgStartIndex +1, tableEndIndex));
                } catch (NumberFormatException nfe) {
                    if (LOGGER.isLoggable(Level.SEVERE)) {
                        LOGGER.severe("Unable to parse the specified EPSGCode. Proceeding with DEFAULT:"
                    + DEFAULT_EPSG_CODE + " due to : " + nfe.getLocalizedMessage());
                    }
                }
            }
           
            final String table = pgrasterUrl.substring(schemaEndIndex + 1, epsgStartIndex != -1 ? epsgStartIndex : tableEndIndex);

            // Parsing options
            final int optionsStartIndex = pgrasterUrl.indexOf("?", tableEndIndex + 1);
            final String options = optionsStartIndex != -1 ? pgrasterUrl.substring(optionsStartIndex +1, fileURLIndex): DEFAULT_OPTIONS;

            // Parsing file extensions of raster to be imported (if any)
            final int fileExtensionEndIndex = optionsStartIndex != -1 ? optionsStartIndex : fileURLIndex;
            final String fileExtension = pgrasterUrl.substring(tableEndIndex + 1, fileExtensionEndIndex);

            Properties datastoreProperties = new Properties();
            datastoreProperties.put(HOST_KEY.substring(1), host);
            datastoreProperties.put(PGUSER_KEY.substring(1), pguser);
            datastoreProperties.put(PORT_KEY.substring(1), port);
            datastoreProperties.put(PASSWORD_KEY.substring(1), password);
            datastoreProperties.put(DATABASE_KEY.substring(1), db);

            JDBCPGrasterConfigurationBean bean = new JDBCPGrasterConfigurationBean(datastoreProperties, table, "rt" + table, fileExtension, table, options, schema, epsgCode);
            return bean;
        }
        return null;
    }

    /**
     * Main mosaic configuration method: given a folder containing several tiles, this static helper method does this:
     * i   - import raster tiles into the database
     * ii  - create metadata table and put new tables on it
     * iii - prepare configuration file from template
     *
     * @param configDir
     * @param configBean
     * @return
     * @throws IOException
     * @throws SQLException
     */
    private URL buildConfiguration() {
        // Step 1: Validate configuration
        validateConfiguration();

        // Step 2: Check for/Create Config file
        final File configFile = new File(DataUtilities.urlToFile(configDir).getAbsolutePath()
                + File.separatorChar + configBean.getCoverageName() + ".pgraster.xml");

        URL url = DataUtilities.fileToURL(configFile);
        Config config = null;
        Connection connection = null;
        final List<File> filesToBeDeleted = new ArrayList<File>();

        if (!configFile.exists()) {
            // Config file doesn't exist. Need to create it
            try {

                // TODO: we may consider adding support for external folder where to store both config file
                // and script file
                createConfigFile(configFile);
                config = Config.readFrom(url);
                DBDialect dialect = DBDialect.getDBDialect(config);
                connection = dialect.getConnection();

                // Step 3: configure raster into DB in case they haven't been imported by hand
                // Manual import may be preferred by the user when dealing with huge datasets
                if (!isMosaicAlreadyInDB(connection)) {
                    if (LOGGER.isLoggable(Level.FINE)) {
                        LOGGER.fine("Proceeding with raster tiles automatic import using raster2pgsql");
                    }
                    importTilesIntoDB(connection, filesToBeDeleted);
                } else {
                    if (LOGGER.isLoggable(Level.INFO)) {
                        LOGGER.info("Skipping raster tiles import " +
                        "since metadata tables and tile tables already exists into the DB");
                    }
                }

            } catch (Exception e) {

                // Rollback on configFile (In case something went wrong, delete the configFile)
                if (configFile.exists()) {
                    configFile.delete();
                }
                throw new RuntimeException(e);
            } finally {
                if (connection != null) {
                    try {
                        connection.close();
                    } catch (Throwable t) {
                        // Does nothing
                    }
                }
                for (File file : filesToBeDeleted) {
                    try {
                        file.delete();
                    } catch (Exception e) {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.fine("Exception occurred while deleting temp file: " + file.getAbsolutePath() + "\n" + e.getLocalizedMessage());
                        }
                    }
                }
            }
        }
        // Return the URL of the configuration xml
        return url;
    }

    /**
     * Check whether the specified mosaic is already available in the Database. In that case I don't need to
     * do the raster tiles import.
     *
     * @param connection the connection to be used to check for the database presence
     * @return
     */
    private boolean isMosaicAlreadyInDB(final Connection connection) {

        final String selectMetadataTableQuery = TABLE_CHECK_SQL.replace((MOSAIC_KEY), configBean.getTableName());
        try {
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("Looking for mosaic table already created");
            }
            final PreparedStatement ps = connection.prepareStatement(selectMetadataTableQuery);
            ResultSet set = ps.executeQuery();
            final boolean allTablesArePresent =  checkTileTables(set, connection);
            connection.commit();
            return allTablesArePresent;
        } catch (SQLException e) {
            // The required table may not exists... We need to create it
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine(e.getLocalizedMessage());
            }
            try {
                connection.rollback();
            } catch (SQLException ex) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine("Exception occurred while doing rollback:\n" + e.getLocalizedMessage());
                }
            }
        }
        return false;
    }

    /**
     * Check whether the main metadata table is present and all the tileTables
     * specified inside it (if any) exists too.
     * In that case, no need to do the raster import step. 
     * @param set the {@link ResultSet} coming from the previous selection made on tileTable column.
     * @param connection
     * @return
     * @throws SQLException
     */
    private boolean checkTileTables(ResultSet set, Connection connection) throws SQLException {
        boolean proceed = true;
        boolean found = false;

        // Check that metadata table exists and all referred tile tables exixst too
        while (set.next() && proceed) {
            // A table with the specified name has been found.
            // Checking whether the raster tile table exist too
            final String tileTableName = set.getString("tiletable");
            final String selectMetadataTableQuery = TILE_TABLE_CHECK_SQL.replace((MOSAIC_KEY), tileTableName);
            try {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine("Looking for mosaic table already created");
                }
                final PreparedStatement ps = connection.prepareStatement(selectMetadataTableQuery);
                ResultSet tileSet = ps.executeQuery();
                if (tileSet.next()) {
                    // We have found the tileTable referred inside metadata table
                    // Therefore, proceeding with the next one
                    if (LOGGER.isLoggable(Level.FINE)) {
                        LOGGER.fine("tile table " + tileTableName + " has been found");
                    }
                    if (set.isLast()) {
                        // I'm checking the latest entry. All tables have been found.
                        // No more checks are needed
                        proceed = false;
                        found = true;
                    }
                    continue;
                }
            } catch (SQLException sqle) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine(sqle.getLocalizedMessage());
                }
                final String message = "Database contains the metadata table but some referred tile tables are missing. \n" +
                        "Please, cleanup your database and retry the configuration";
                // Logging a message reporting that the database is inconsistent (metadata table exists, but only a few
                // referred raster tile table actually exist). We need some user intervention
                if (LOGGER.isLoggable(Level.SEVERE)) {
                    LOGGER.severe(message);
                }
                throw new IllegalArgumentException(message);
            }
        }
       
        return found;
    }

    /**
     * Check that all the configuration parameters are available
     *
     * @param configBean
     */
    private void validateConfiguration() {
        Utilities.ensureNonNull("configBean", configBean);

        final Properties properties = configBean.getDatastoreProperties();
        Utilities.ensureNonNull("datastoreProperties", properties);

        final String schema = configBean.getSchema();
        Utilities.ensureNonNull("schema", schema);

        final String tileTablePrefix = configBean.getTileTablePrefix();
        Utilities.ensureNonNull("tileTablePrefix", tileTablePrefix);

        final String coverageName = configBean.getCoverageName();
        Utilities.ensureNonNull("coverageName", coverageName);

        final String tableName = configBean.getTableName();
        Utilities.ensureNonNull("tableName", tableName);
    }

    /**
     * Create the metadata table which will contain the tile tables references.
     *
     * @param connection
     * @param tileTables the tile Table names
     * @param tableName the name of the master table where tile tables will be added
     * @param coverageName the name of the coverage to which tile tables are related
     * @throws SQLException
     */
    private void createMetadataTable(final Connection connection, final String tableName,
            final String coverageName, final List<String> tileTables){

        // Prepare main insertion/update queries
        final String createMetadataTableQuery = TABLE_CREATION_SQL.replace((MOSAIC_KEY), tableName);
        final String insertTileQuery = TILETABLE_INSERTION_SQL.replace((MOSAIC_KEY), tableName);
        boolean created = false;
        try {
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("Creating mosaic table");
            }
            // Create the metadata table
            connection.prepareStatement(createMetadataTableQuery).execute();
            created = true;

            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("updating mosaic table");
            }
            final PreparedStatement ps = connection.prepareStatement(insertTileQuery);

            // Inserting tile tables
            for (String tileTable : tileTables) {
                ps.setString(1, coverageName);
                ps.setString(2, tileTable);
                ps.execute();
            }

            // commit
            connection.commit();
        } catch (SQLException e) {
            if (LOGGER.isLoggable(Level.SEVERE)) {
                LOGGER.severe("Exception occurred while " + (created? "updating":"creating") + " metadata tables. Proceeding with rollback\n" + e.getLocalizedMessage());
            }
            try {
                connection.rollback();
            } catch (SQLException ex) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine("Exception occurred while doing rollback:\n" + e.getLocalizedMessage());
                }
            }
        }
    }

    /**
     * Populate the DB with rasters produced in advance by the user with gdal_retile
     * @param connection
     * @param filesToBeDeleted a List which will contains files to be deleted
     * @return
     * @throws SQLException
     * @throws IOException
     */
    private void importTilesIntoDB(final Connection connection, final List<File> filesToBeDeleted) throws SQLException, IOException {
        final File configDirectory = DataUtilities.urlToFile(configDir);

        // Preliminary check on configuration directory validity
        if (!configDirectory.exists()) {
            throw new IllegalArgumentException("Specified URL doesn't exist: " + configDir);
        }

        if (!configDirectory.isDirectory()) {
            throw new IllegalArgumentException("Specified URL doesn't refer to a directory: "
                    + configDir);
        }

        // Preparing main configuration parameters
        final String tablePrefix = configBean.getTileTablePrefix();
        final String importOptions = configBean.getImportOptions();
        final String fileExtension = configBean.getFileExtension();
        final String schema = configBean.getSchema();

        // Database properties
        final Properties datastoreProperties = configBean.getDatastoreProperties();
        final String database = (String) datastoreProperties.get(DATABASE_KEY.substring(1));
        final String pguser = (String) datastoreProperties.get(PGUSER_KEY.substring(1));
        final String password = (String) datastoreProperties.get(PASSWORD_KEY.substring(1));
        final String port = (String) datastoreProperties.get(PORT_KEY.substring(1));
        final String host = (String) datastoreProperties.get(HOST_KEY.substring(1));

        final List<String> tileTables = new ArrayList<String>();

        // scan tiles folder created by gdal_retile
        // Note that GDAL put tiles for level 0 straight on the folder.
        // additional levels are stored on folder 1, 2, 3 and so on

        final File dataDir = DataUtilities.urlToFile(configDir);

        // Preparing a single script containing all the raster2pgsql commands to be invoked
        String script = createScript(dataDir, database, schema, host, port, pguser, tablePrefix, fileExtension, tileTables, importOptions, filesToBeDeleted);

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Executing the script");
        }
        executeScript(dataDir, script, password, filesToBeDeleted);

        // Step 2: create mosaic table for metadata and insert tile tables into metadata table
        final String tableName = configBean.getTableName();
        final String coverageName = configBean.getCoverageName();
       
        //TODO: We should check previous table exists before creating metadata table
        createMetadataTable(connection, tableName, coverageName, tileTables);
    }

    /**
     * Create a shell script that does raster2pgsql invokations which do the raster tiles import into the PGRaster database
     * @param dataDir the main datadir (containing tiles)
     * @param database the database
     * @param schema the schema where tiles need to be added
     * @param host the postgres DB server
     * @param port the postgres DB server port
     * @param pguser the postgres user
     * @param tablePrefix the prefix to be put at the head of each table name containing tiles at different levels
     * @param fileExtension the file extension of the raster files to be imported (as an instance, *.png or *.tif)
     * @param tileTables a List where table names will be added.
     * @param importOptions
     * @param filesToBeDeleted a List of files which need to be deleted at the end of the import
     * @return the script content as a String
     */
    private static String createScript(final File dataDir, final String database, final String schema, final String host, final String port,
            final String pguser, final String tablePrefix, final String fileExtension, final List<String> tileTables,
            final String importOptions, List<File> filesToBeDeleted) {
        // Prepare the raster2pgsql command by replacing values
        final String mainCommand = prepareMainCommand(importOptions, fileExtension, database, pguser,
                schema, host, port);

        final StringBuilder commands = new StringBuilder();
        final File[] files = dataDir.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                // scan folders for tiles to be imported
                final String importCommand = updateCommand(file, mainCommand, tablePrefix,
                        fileExtension, tileTables, filesToBeDeleted);
                commands.append(importCommand).append(LINE_SEPARATOR);
            }
        }
        String importCommand = updateCommand(dataDir, mainCommand, tablePrefix, fileExtension,
                tileTables, filesToBeDeleted);
        commands.append(importCommand).append(LINE_SEPARATOR);
        return commands.toString();
    }

    /**
     * Execute all the raster2pgsql import commands in one step
     * @param dataDir
     * @param script the script content
     * @param password
     * @param filesToBeDeleted
     * @throws IOException
     */
    private void executeScript(final File dataDir, final String script, final String password, final List<File> filesToBeDeleted) throws IOException {
        final File scriptFile = new File(dataDir, EXECUTE);
        filesToBeDeleted.add(scriptFile);
        writeToFile(scriptFile, script);

        ExecTask task = new ExecTask();
        task.setExecutable(scriptFile.getAbsolutePath());
        task.setDir(dataDir);
        // Setting Postgres password to authenticate DB Import
        Variable variable = new Variable();
        variable.setKey("PGPASSWORD");
        variable.setValue(password);
        task.addEnv(variable);

        // Setting PATH variable
        Variable varPath = new Variable();
        varPath.setKey("PATH");
        varPath.setValue(PATH);
        task.addEnv(variable);
        task.execute();
    }

    /**
     * Replace the main command with proper values for each argument.
     *
     * @param file the folder containing files to be imported
     * @param mainCommand the original command with $PARAM to be replaced
     * @param tablePrefix the prefix of the table
     * @param fileExtension the file type do be inserted
     * @param tileTables
     * @param filesToBeDeleted a List of files to be deleted at the end of import step
     * @return
     */
    private static String updateCommand(File file, String mainCommand, String tablePrefix, String fileExtension, List<String> tileTables, List<File> filesToBeDeleted) {
        final String folderName = file.getName();
        final String tileTable = tablePrefix + folderName;
        tileTables.add(tileTable);
        String command = mainCommand.replace(TABLE_PREFIX_KEY, tileTable);
        command = command.replace(FILES_KEY, file.getAbsolutePath() + File.separatorChar + fileExtension);
        final String fileToBeDeleted = file.getAbsolutePath() + File.separatorChar + tileTable + ".sql";
        filesToBeDeleted.add(new File(fileToBeDeleted));
        command = command.replace(SQL_FILE_KEY, fileToBeDeleted);
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Import Script to be executed: " + command);
        }
        return command;
    }

    /**
     * Prepare the main raster2pgsql command by updating parameters
     * @param importOptions
     * @param fileExtension
     * @param database
     * @param pguser
     * @param schema
     * @return
     */
    private static String prepareMainCommand(String importOptions,
            String fileExtension, String database, String pguser, String schema, String host, String port) {
          String mainCommand = IMPORT_COMMAND;

        if (importOptions == null) {
            importOptions = DEFAULT_OPTIONS;
        }
        mainCommand = mainCommand.replace(OPTIONS_KEY, importOptions);
        mainCommand = mainCommand.replace(SCHEMA_KEY, schema);
        mainCommand = mainCommand.replace(DATABASE_KEY, database);
        mainCommand = mainCommand.replace(PGUSER_KEY, pguser);
        mainCommand = mainCommand.replace(HOST_KEY, host);
        mainCommand = mainCommand.replace(PORT_KEY, port);
       
        return mainCommand;
    }

    /**
     * Create the configuration file containing the information to configure the ImageMosaic
     *
     * @param configFile the file where to store the configuration
     * @param configBean the bean with configuration parameters
     * @return
     * @throws IOException
     */
    private void createConfigFile(final File configFile) throws IOException {
        final String config = updateValues(configBean);
        storeConfig(configFile, config);
    }

    /**
     * Replace all the Jolly Strings from the Template with actual values.
     *
     * @param configBean the Configuration bean containing custom values to replace the jolly.
     * @return
     */
    private String updateValues(JDBCPGrasterConfigurationBean configBean) {
        Properties datastoreProperties = configBean.getDatastoreProperties();
        String config = TEMPLATE.replace(MOSAIC_KEY, configBean.getTableName())
                .replace(COVERAGE_KEY, configBean.getCoverageName())
                .replace(PGUSER_KEY, datastoreProperties.getProperty(PGUSER_KEY.substring(1)))
                .replace(DATABASE_KEY, datastoreProperties.getProperty(DATABASE_KEY.substring(1)))
                .replace(PASSWORD_KEY, datastoreProperties.getProperty(PASSWORD_KEY.substring(1)))
                .replace(PORT_KEY, datastoreProperties.getProperty(PORT_KEY.substring(1)))
                .replace(HOST_KEY, datastoreProperties.getProperty(HOST_KEY.substring(1)))
                .replace(EPSG_CODE_KEY, Integer.toString(configBean.getEpsgCode()));
        return config;
    }

    /**
     * Store the configuration as an XML files containing all the config params.
     * @param configFile the output file where to store the config
     * @param config the configuration XML String to be stored
     *
     * @return
     * @throws IOException
     */
    private void storeConfig(final File configFile, final String config) throws IOException {
        writeToFile(configFile, config);
    }

    /**
     * Write a String content to the specified file.
     * @param file the file where to store the content
     * @param content the string to be written
     * @throws IOException
     */
    private static void writeToFile(final File file, final String content) throws IOException {
        OutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(file);
            outputStream.write(content.getBytes());
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (Exception ignored) {

                }
            }
        }
    }
}
TOP

Related Classes of org.geotools.gce.imagemosaic.jdbc.custom.JDBCPGRasterConfigurationBuilder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.