<commit_before>/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2005-2008, 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.referencing.factory.epsg;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.sql.DataSource;

import org.geotools.factory.Hints;
import org.geotools.referencing.factory.AbstractAuthorityFactory;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.LoggingKeys;
import org.geotools.resources.i18n.Loggings;
import org.geotools.util.Version;
import org.geotools.util.logging.Logging;
import org.hsqldb.jdbc.JDBCDataSource;
import org.opengis.referencing.FactoryException;


/**
 * Connection to the EPSG database in HSQL database engine format using JDBC. The EPSG
 * database can be downloaded from <A HREF="http://www.epsg.org">http://www.epsg.org</A>.
 * The SQL scripts (modified for the HSQL syntax as <A HREF="doc-files/HSQL.html">explained
 * here</A>) are bundled into this plugin. The database version is given in the
 * {@linkplain org.opengis.metadata.citation.Citation#getEdition edition attribute}
 * of the {@linkplain org.opengis.referencing.AuthorityFactory#getAuthority authority}.
 * The HSQL database is read only.
 * <P>
 * <H3>Implementation note</H3>
 * The SQL scripts are executed the first time a connection is required. The database
 * is then created as cached tables ({@code HSQL.properties} and {@code HSQL.data} files)
 * in a temporary directory. Future connections to the EPSG database while reuse the cached
 * tables, if available. Otherwise, the scripts will be executed again in order to recreate
 * them.
 * <p>
 * If the EPSG database should be created in a different directory (or already exists in that
 * directory), it may be specified as a {@linkplain System#getProperty(String) system property}
 * nammed {@value #DIRECTORY_KEY}.
 *
 * @since 2.4
 *
 *
 * @source $URL$
 * @version $Id$
 * @author <NAME>
 * @author <NAME>
 */
public class ThreadedHsqlEpsgFactory extends ThreadedEpsgFactory {
    /**
     * Current version of EPSG-HSQL plugin. This is usually the same version number than the
     * one in the EPSG database bundled in this plugin. However this field may contains
     * additional minor version number if there is some changes related to the EPSG-HSQL
     * plugin rather then the EPSG database itself (for example additional database index).
     */
    public static final Version VERSION = new Version("8.6.0.0");

    /**
     * The key for fetching the database directory from {@linkplain System#getProperty(String)
     * system properties}.
     */
    public static final String DIRECTORY_KEY = "EPSG-HSQL.directory";

    /**
     * The name of the ZIP file to read in order to create the cached database.
     */
    private static final String ZIP_FILE = "EPSG.zip";

    /**
     * The database name.
     */
    public static final String DATABASE_NAME = "EPSG";
    
    /**
     * The successful database creation marker
     */
    static final String MARKER_FILE = "EPSG_creation_marker.txt";
    
    /**
     * The database creation lock file
     */
    static final String LOCK_FILE = "EPSG_creation_lock.txt";

    /**
     * The prefix to put in front of URL to the database.
     */
    static final String PREFIX = "jdbc:hsqldb:file:";

    /**
     * The logger name.
     */
    static final String LOGGER = "org.geotools.referencing.factory.epsg";

    /**
     * Creates a new instance of this factory. If the {@value #DIRECTORY_KEY}
     * {@linkplain System#getProperty(String) system property} is defined and contains
     * the name of a directory with a valid {@linkplain File#getParent parent}, then the
     * {@value #DATABASE_NAME} database will be saved in that directory. Otherwise, a
     * temporary directory will be used.
     */
    public ThreadedHsqlEpsgFactory() {
        this(null);
    }

    /**
     * Creates a new instance of this data source using the specified hints. The priority
     * is set to a lower value than the {@linkplain FactoryOnAccess}'s one in order to give
     * precedence to the Access-backed database, if presents. Priorities are set that way
     * because:
     * <ul>
     *   <li>The MS-Access format is the primary EPSG database format.</li>
     *   <li>If a user downloads the MS-Access database himself, he probably wants to use it.</li>
     * </ul>
     */
    public ThreadedHsqlEpsgFactory(final Hints hints) {
        super(hints, PRIORITY + 1);
    }

    /**
     * Returns the default directory for the EPSG database. If the {@value #DIRECTORY_KEY}
     * {@linkplain System#getProperty(String) system property} is defined and contains the
     * name of a directory with a valid {@linkplain File#getParent parent}, then the
     * {@value #DATABASE_NAME} database will be saved in that directory. Otherwise,
     * a temporary directory will be used.
     */
    private static File getDirectory() {
        try {
            final String property = System.getProperty(DIRECTORY_KEY);
            if (property != null) {
                final File directory = new File(property);
                /*
                 * Creates the directory if needed (mkdir), but NOT the parent directories (mkdirs)
                 * because a missing parent directory may be a symptom of an installation problem.
                 * For example if 'directory' is a subdirectory in the temporary directory (~/tmp/),
                 * this temporary directory should already exists. If it doesn't, an administrator
                 * should probably looks at this problem.
                 */
                if (directory.isDirectory() || directory.mkdir()) {
                    return directory;
                }
            }
        } catch (SecurityException e) {
            /*
             * Can't fetch the base directory from system properties.
             * Fallback on the default temporary directory.
             */
        }
        return getTemporaryDirectory();
    }

    /**
     * Returns the directory to uses in the temporary directory folder.
     */
    private static File getTemporaryDirectory() {
        File directory = new File(System.getProperty("java.io.tmpdir", "."), "Geotools");
        if (directory.isDirectory() || directory.mkdir()) {
            directory = new File(directory, "Databases/HSQL");
            if (directory.isDirectory() || directory.mkdirs()) {
                return directory;
            }
        }
        return null;
    }

    /**
     * Extract the directory from the specified data source, or {@code null} if this
     * information is not available.
     */
    private static File getDirectory(final DataSource source) {
        if (source instanceof JDBCDataSource) {
            String path = ((JDBCDataSource) source).getDatabase();
            if (path!=null && PREFIX.regionMatches(true, 0, path, 0, PREFIX.length())) {
                path = path.substring(PREFIX.length());
                return new File(path).getParentFile();
            }
        }
        return null;
    }

    /**
     * Returns a data source for the HSQL database.
     */
    protected DataSource createDataSource() throws SQLException {
        final Logger logger = Logging.getLogger(LOGGER);
        logger.log(Level.FINE, "Building new data source for " + getClass().getName());
        
        DataSource candidate = super.createDataSource();
        if (candidate instanceof JDBCDataSource) {
            return candidate;
        }
        final JDBCDataSource source = new JDBCDataSource();
        File directory = new File(getDirectory(), "v" + VERSION);
        if (directory != null) {
            /*
             * Constructs the full path to the HSQL database. Note: we do not use
             * File.toURI() because HSQL doesn't seem to expect an encoded URL
             * (e.g. "%20" instead of spaces).
             */
            final StringBuilder url = new StringBuilder(PREFIX);
            final String path = directory.getAbsolutePath().replace(File.separatorChar, '/');
            if (path.length()==0 || path.charAt(0)!='/') {
                url.append('/');
            }
            url.append(path);
            if (url.charAt(url.length()-1) != '/') {
                url.append('/');
            }
            url.append(DATABASE_NAME);
            url.append(";shutdown=true");
            source.setDatabase(url.toString());
            assert directory.equals(getDirectory(source)) : url;
        }
        /*
         * If the temporary directory do not exists or can't be created, lets the 'database'
         * attribute unset. If the user do not set it explicitly (through JNDI or by overrding
         * this method), an exception will be thrown when 'createBackingStore()' will be invoked.
         */
        source.setUser("SA"); // System administrator. No password.
        return source;
    }

    /**
     * Returns {@code true} if the database contains data. This method returns {@code false}
     * if an empty EPSG database has been automatically created by HSQL and not yet populated.
     */
    private static boolean dataExists(File directory) throws SQLException {
        // check if the marker file is there, and all the other database files as well
        // (as some windows cleanup tools delete the .data file only)
        return new File(directory, MARKER_FILE).exists() &&
               new File(directory, DATABASE_NAME + ".data").exists() &&
               new File(directory, DATABASE_NAME + ".properties").exists() &&
               new File(directory, DATABASE_NAME + ".script").exists();
    }

    /**
     * Returns the backing-store factory for HSQL syntax. If the cached tables are not available,<commit_msg>Make sure we connect to the db as read only<commit_after>/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2005-2008, 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.referencing.factory.epsg;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.sql.DataSource;

import org.geotools.factory.Hints;
import org.geotools.referencing.factory.AbstractAuthorityFactory;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.LoggingKeys;
import org.geotools.resources.i18n.Loggings;
import org.geotools.util.Version;
import org.geotools.util.logging.Logging;
import org.hsqldb.jdbc.JDBCDataSource;


/**
 * Connection to the EPSG database in HSQL database engine format using JDBC. The EPSG
 * database can be downloaded from <A HREF="http://www.epsg.org">http://www.epsg.org</A>.
 * The SQL scripts (modified for the HSQL syntax as <A HREF="doc-files/HSQL.html">explained
 * here</A>) are bundled into this plugin. The database version is given in the
 * {@linkplain org.opengis.metadata.citation.Citation#getEdition edition attribute}
 * of the {@linkplain org.opengis.referencing.AuthorityFactory#getAuthority authority}.
 * The HSQL database is read only.
 * <P>
 * <H3>Implementation note</H3>
 * The SQL scripts are executed the first time a connection is required. The database
 * is then created as cached tables ({@code HSQL.properties} and {@code HSQL.data} files)
 * in a temporary directory. Future connections to the EPSG database while reuse the cached
 * tables, if available. Otherwise, the scripts will be executed again in order to recreate
 * them.
 * <p>
 * If the EPSG database should be created in a different directory (or already exists in that
 * directory), it may be specified as a {@linkplain System#getProperty(String) system property}
 * nammed {@value #DIRECTORY_KEY}.
 *
 * @since 2.4
 *
 *
 * @source $URL$
 * @version $Id$
 * @author <NAME>
 * @author <NAME>
 */
public class ThreadedHsqlEpsgFactory extends ThreadedEpsgFactory {
    /**
     * Current version of EPSG-HSQL plugin. This is usually the same version number than the
     * one in the EPSG database bundled in this plugin. However this field may contains
     * additional minor version number if there is some changes related to the EPSG-HSQL
     * plugin rather then the EPSG database itself (for example additional database index).
     */
    public static final Version VERSION = new Version("8.6.0.0");

    /**
     * The key for fetching the database directory from {@linkplain System#getProperty(String)
     * system properties}.
     */
    public static final String DIRECTORY_KEY = "EPSG-HSQL.directory";

    /**
     * The name of the ZIP file to read in order to create the cached database.
     */
    private static final String ZIP_FILE = "EPSG.zip";

    /**
     * The database name.
     */
    public static final String DATABASE_NAME = "EPSG";
    
    /**
     * The successful database creation marker
     */
    static final String MARKER_FILE = "EPSG_creation_marker.txt";
    
    /**
     * The database creation lock file
     */
    static final String LOCK_FILE = "EPSG_creation_lock.txt";

    /**
     * The prefix to put in front of URL to the database.
     */
    static final String PREFIX = "jdbc:hsqldb:file:";

    /**
     * The logger name.
     */
    static final String LOGGER = "org.geotools.referencing.factory.epsg";

    /**
     * Creates a new instance of this factory. If the {@value #DIRECTORY_KEY}
     * {@linkplain System#getProperty(String) system property} is defined and contains
     * the name of a directory with a valid {@linkplain File#getParent parent}, then the
     * {@value #DATABASE_NAME} database will be saved in that directory. Otherwise, a
     * temporary directory will be used.
     */
    public ThreadedHsqlEpsgFactory() {
        this(null);
    }

    /**
     * Creates a new instance of this data source using the specified hints. The priority
     * is set to a lower value than the {@linkplain FactoryOnAccess}'s one in order to give
     * precedence to the Access-backed database, if presents. Priorities are set that way
     * because:
     * <ul>
     *   <li>The MS-Access format is the primary EPSG database format.</li>
     *   <li>If a user downloads the MS-Access database himself, he probably wants to use it.</li>
     * </ul>
     */
    public ThreadedHsqlEpsgFactory(final Hints hints) {
        super(hints, PRIORITY + 1);
    }

    /**
     * Returns the default directory for the EPSG database. If the {@value #DIRECTORY_KEY}
     * {@linkplain System#getProperty(String) system property} is defined and contains the
     * name of a directory with a valid {@linkplain File#getParent parent}, then the
     * {@value #DATABASE_NAME} database will be saved in that directory. Otherwise,
     * a temporary directory will be used.
     */
    private static File getDirectory() {
        try {
            final String property = System.getProperty(DIRECTORY_KEY);
            if (property != null) {
                final File directory = new File(property);
                /*
                 * Creates the directory if needed (mkdir), but NOT the parent directories (mkdirs)
                 * because a missing parent directory may be a symptom of an installation problem.
                 * For example if 'directory' is a subdirectory in the temporary directory (~/tmp/),
                 * this temporary directory should already exists. If it doesn't, an administrator
                 * should probably looks at this problem.
                 */
                if (directory.isDirectory() || directory.mkdir()) {
                    return directory;
                }
            }
        } catch (SecurityException e) {
            /*
             * Can't fetch the base directory from system properties.
             * Fallback on the default temporary directory.
             */
        }
        return getTemporaryDirectory();
    }

    /**
     * Returns the directory to uses in the temporary directory folder.
     */
    private static File getTemporaryDirectory() {
        File directory = new File(System.getProperty("java.io.tmpdir", "."), "Geotools");
        if (directory.isDirectory() || directory.mkdir()) {
            directory = new File(directory, "Databases/HSQL");
            if (directory.isDirectory() || directory.mkdirs()) {
                return directory;
            }
        }
        return null;
    }

    /**
     * Extract the directory from the specified data source, or {@code null} if this
     * information is not available.
     */
    private static File getDirectory(final DataSource source) {
        if (source instanceof JDBCDataSource) {
            String path = ((JDBCDataSource) source).getDatabase();
            if (path!=null && PREFIX.regionMatches(true, 0, path, 0, PREFIX.length())) {
                path = path.substring(PREFIX.length());
                return new File(path).getParentFile();
            }
        }
        return null;
    }

    /**
     * Returns a data source for the HSQL database.
     */
    protected DataSource createDataSource() throws SQLException {
        final Logger logger = Logging.getLogger(LOGGER);
        logger.log(Level.FINE, "Building new data source for " + getClass().getName());
        
        DataSource candidate = super.createDataSource();
        if (candidate instanceof JDBCDataSource) {
            return candidate;
        }
        final JDBCDataSource source = new JDBCDataSource();
        File directory = new File(getDirectory(), "v" + VERSION);
        if (directory != null) {
            /*
             * Constructs the full path to the HSQL database. Note: we do not use
             * File.toURI() because HSQL doesn't seem to expect an encoded URL
             * (e.g. "%20" instead of spaces).
             */
            final StringBuilder url = new StringBuilder(PREFIX);
            final String path = directory.getAbsolutePath().replace(File.separatorChar, '/');
            if (path.length()==0 || path.charAt(0)!='/') {
                url.append('/');
            }
            url.append(path);
            if (url.charAt(url.length()-1) != '/') {
                url.append('/');
            }
            url.append(DATABASE_NAME);
            url.append(";shutdown=true;readonly=true");
            source.setDatabase(url.toString());
            assert directory.equals(getDirectory(source)) : url;
        }
        /*
         * If the temporary directory do not exists or can't be created, lets the 'database'
         * attribute unset. If the user do not set it explicitly (through JNDI or by overrding
         * this method), an exception will be thrown when 'createBackingStore()' will be invoked.
         */
        source.setUser("SA"); // System administrator. No password.
        return source;
    }

    /**
     * Returns {@code true} if the database contains data. This method returns {@code false}
     * if an empty EPSG database has been automatically created by HSQL and not yet populated.
     */
    private static boolean dataExists(File directory) throws SQLException {
        // check if the marker file is there, and all the other database files as well
        // (as some windows cleanup tools delete the .data file only)
        return new File(directory, MARKER_FILE).exists() &&
               new File(directory, DATABASE_NAME + ".data").exists() &&
               new File(directory, DATABASE_NAME + ".properties").exists() &&
               new File(directory, DATABASE_NAME + ".script").exists();
    }

    /**
     * Returns the backing-store factory for HSQL syntax. If the cached tables are not available,
{'fraction_non_alphanumeric': 0.09375727386993157, 'fraction_numerical': 0.0031655881942181463, 'mean_word_length': 2.5725927157824713, 'pattern_counts': {'":': 0, '<': 35, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 2, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6351723', 'n_tokens_mistral': 5793, 'n_tokens_neox': 5591, 'n_words': 2082}