blob: 8b794d233d8c44dd0a78dec1d17afe799e88f352 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.referencing.factory.sql;
import java.util.Set;
import java.util.Locale;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.sql.Connection;
import java.io.BufferedReader;
import java.io.LineNumberReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.setup.InstallationResources;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.referencing.Fallback;
import org.apache.sis.internal.system.DataDirectory;
import org.apache.sis.internal.system.Loggers;
import org.apache.sis.internal.util.CollectionsExt;
import org.apache.sis.internal.util.Constants;
/**
* Provides SQL scripts needed for creating a local copy of a dataset. This class allows Apache SIS users
* to bundle the EPSG or other datasets in their own product for automatic installation when first needed.
* Implementations of this class can be declared in the following file for automatic discovery by {@link EPSGFactory}:
*
* {@preformat text
* META-INF/services/org.apache.sis.setup.InstallationResources
* }
*
* <h2>How this class is used</h2>
* The first time that an {@link EPSGDataAccess} needs to be instantiated,
* {@link EPSGFactory} verifies if the EPSG database exists. If it does not, then:
* <ol>
* <li>{@link EPSGFactory#install(Connection)} searches for the first instance of {@link InstallationResources}
* (the parent of this class) for which the {@linkplain #getAuthorities() set of authorities} contains {@code "EPSG"}.</li>
* <li>The {@linkplain #getLicense license} may be shown to the user if the application allows that
* (for example when running as a {@linkplain org.apache.sis.console console application}).</li>
* <li>If the installation process is allowed to continue, it will iterate over all readers provided by
* {@link #openScript(String, int)} and execute the SQL statements (not necessarily verbatim;
* the installation process may adapt to the target database).</li>
* </ol>
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.7
* @since 0.7
* @module
*/
public abstract class InstallationScriptProvider extends InstallationResources {
/**
* A sentinel value for the content of the script to execute before the SQL scripts provided by the authority.
* This is an Apache SIS build-in script for constraining the values of some {@code VARCHAR} columns
* to enumerations of values recognized by {@link EPSGDataAccess}. Those enumerations are not required
* for proper working of {@link EPSGFactory}, but can improve data integrity.
*/
protected static final String PREPARE = "Prepare";
/**
* A sentinel value for the content of the script to execute after the SQL scripts provided by the authority.
* This is an Apache SIS build-in script for creating indexes or performing any other manipulation that help
* SIS to use the dataset. Those indexes are not required for proper working of {@link EPSGFactory},
* but can significantly improve performances.
*/
protected static final String FINISH = "Finish";
/**
* The authorities to be returned by {@link #getAuthorities()}.
*/
private final Set<String> authorities;
/**
* The names of the SQL scripts to read.
*/
private final String[] resources;
/**
* Creates a new provider which will read script files of the given names in that order.
* The given names are often filenames, but not necessarily
* (it is okay to use those names only as labels).
*
* <table class="sis">
* <caption>Typical argument values</caption>
* <tr>
* <th>Authority</th>
* <th class="sep">Argument values</th>
* </tr><tr>
* <td>{@code EPSG}</td>
* <td class="sep"><code>
* {{@linkplain #PREPARE}, "EPSG_Tables.sql", "EPSG_Data.sql", "EPSG_FKeys.sql", {@linkplain #FINISH}}
* </code></td>
* </tr>
* </table>
*
* @param authority the authority (typically {@code "EPSG"}), or {@code null} if not available.
* @param resources names of the SQL scripts to read.
*
* @see #getResourceNames(String)
* @see #openStream(String)
*/
protected InstallationScriptProvider(final String authority, final String... resources) {
ArgumentChecks.ensureNonNull("resources", resources);
authorities = CollectionsExt.singletonOrEmpty(authority);
this.resources = resources;
}
/**
* Returns the identifiers of the dataset installed by the SQL scripts.
* The values currently recognized by SIS are:
*
* <ul>
* <li>{@code "EPSG"} for the EPSG geodetic dataset.</li>
* </ul>
*
* The default implementation returns the authority given at construction time, or an empty set
* if that authority was {@code null}. An empty set means that the provider does not have all
* needed resources or does not have permission to distribute the installation scripts.
*
* @return identifiers of SQL scripts that this instance can distribute.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public Set<String> getAuthorities() {
return authorities;
}
/**
* Verifies that the given authority is one of the expected values.
*/
private void verifyAuthority(final String authority) {
if (!authorities.contains(authority)) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "authority", authority));
}
}
/**
* Returns the names of all SQL scripts to execute.
* This is a copy of the array of names given to the constructor.
* Those names are often filenames, but not necessarily (they may be just labels).
*
* @param authority the value given at construction time (e.g. {@code "EPSG"}).
* @return the names of all SQL scripts to execute.
* @throws IllegalArgumentException if the given {@code authority} argument is not the expected value.
* @throws IOException if fetching the script names required an I/O operation and that operation failed.
*/
@Override
public String[] getResourceNames(String authority) throws IOException {
verifyAuthority(authority);
return resources.clone();
}
/**
* Returns a reader for the SQL script at the given index. Contents may be read from files in a local directory,
* or from resources in a JAR file, or from entries in a ZIP file, or any other means at implementer choice.
* The {@link BufferedReader} instances shall be closed by the caller.
*
* <h4>EPSG case</h4>
* In the EPSG dataset case, the iterator should return {@code BufferedReader} instances for the following files
* (replace {@code <version>} by the EPSG version number and {@code <product>} by the target database) in same order.
* The first and last files are provided by Apache SIS.
* All other files can be downloaded from <a href="http://www.epsg.org/">http://www.epsg.org/</a>.
*
* <ol>
* <li>Content of {@link #PREPARE}, an optional data definition script that define the enumerations expected by {@link EPSGDataAccess}.</li>
* <li>Content of {@code "EPSG_<version>.mdb_Tables_<product>.sql"}, a data definition script that create empty tables.</li>
* <li>Content of {@code "EPSG_<version>.mdb_Data_<product>.sql"}, a data manipulation script that populate the tables.</li>
* <li>Content of {@code "EPSG_<version>.mdb_FKeys_<product>.sql"}, a data definition script that create foreigner key constraints.</li>
* <li>Content of {@link #FINISH}, an optional data definition and data control script that create indexes and set permissions.</li>
* </ol>
*
* Implementers are free to return a different set of scripts with equivalent content.
*
* <h4>Default implementation</h4>
* The default implementation invokes {@link #openStream(String)} – except for {@link #PREPARE} and {@link #FINISH}
* in which case an Apache SIS build-in script is used – and wrap the result in a {@link LineNumberReader}.
* The file encoding is UTF-8 (the encoding used in the scripts distributed by EPSG since version 9.4).
*
* @param authority the value given at construction time (e.g. {@code "EPSG"}).
* @param resource index of the SQL script to read, from 0 inclusive to
* <code>{@linkplain #getResourceNames getResourceNames}(authority).length</code> exclusive.
* @return a reader for the content of SQL script to execute.
* @throws IllegalArgumentException if the given {@code authority} argument is not the expected value.
* @throws IndexOutOfBoundsException if the given {@code resource} argument is out of bounds.
* @throws FileNotFoundException if the SQL script of the given name has not been found.
* @throws IOException if an error occurred while creating the reader.
*/
@Override
public BufferedReader openScript(final String authority, final int resource) throws IOException {
verifyAuthority(authority);
ArgumentChecks.ensureValidIndex(resources.length, resource);
if (!Constants.EPSG.equals(authority)) {
throw new IllegalStateException(Resources.format(Resources.Keys.UnknownAuthority_1, authority));
}
String name = resources[resource];
final InputStream in;
if (PREPARE.equals(name) || FINISH.equals(name)) {
name = authority + '_' + name + ".sql";
in = InstallationScriptProvider.class.getResourceAsStream(name);
} else {
in = openStream(name);
name = name.concat(".sql");
}
if (in == null) {
throw new FileNotFoundException(Errors.format(Errors.Keys.FileNotFound_1, name));
}
return new LineNumberReader(new InputStreamReader(in, StandardCharsets.UTF_8));
}
/**
* Opens the input stream for the SQL script of the given name.
* This method is invoked by the default implementation of {@link #openScript(String, int)}
* for all scripts except {@link #PREPARE} and {@link #FINISH}.
*
* <div class="note"><b>Example 1:</b>
* if this {@code InstallationScriptProvider} instance gets the SQL scripts from files in a well-known directory
* and if the names given at {@linkplain #InstallationScriptProvider(String, String...) construction time} are the
* filenames in that directory, then this method can be implemented as below:
*
* {@preformat java
* protected InputStream openStream(String name) throws IOException {
* return Files.newInputStream(directory.resolve(name));
* }
* }
* </div>
*
* <div class="note"><b>Example 2:</b>
* if this {@code InstallationScriptProvider} instance rather gets the SQL scripts from resources bundled
* in the same JAR files than and in the same package, then this method can be implemented as below:
*
* {@preformat java
* protected InputStream openStream(String name) {
* return MyClass.getResourceAsStream(name);
* }
* }
* </div>
*
* @param name name of the script file to open. Can be {@code null} if the resource is not found.
* @return an input stream opened of the given script file.
* @throws IOException if an error occurred while opening the file.
*/
protected abstract InputStream openStream(final String name) throws IOException;
/**
* Logs the given record. This method pretend that the record has been logged by
* {@code EPSGFactory.install(…)} because it is the public API using this class.
*/
static void log(final LogRecord record) {
record.setLoggerName(Loggers.CRS_FACTORY);
Logging.log(EPSGFactory.class, "install", record);
}
/**
* The default implementation which use the scripts in the {@code $SIS_DATA/Databases/ExternalSources}
* directory, if present. This class expects the files to have those exact names where {@code *} stands
* for any characters provided that there is no ambiguity:
*
* <ul>
* <li>{@code EPSG_*Tables.sql}</li>
* <li>{@code EPSG_*Data.sql}</li>
* <li>{@code EPSG_*FKeys.sql}</li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.7
* @since 0.7
* @module
*/
@Fallback
static final class Default extends InstallationScriptProvider {
/**
* The directory containing the scripts, or {@code null} if it does not exist.
*/
private Path directory;
/**
* Index of the first real file in the array given to the constructor.
* We set the value to 1 for skipping the {@code PREPARE} pseudo-file.
*/
private static final int FIRST_FILE = 1;
/**
* Creates a default provider.
*
* @param locale the locale for warning messages, if any.
*/
Default(final Locale locale) throws IOException {
super(Constants.EPSG,
PREPARE,
"Tables",
"Data",
"FKeys",
FINISH);
Path dir = DataDirectory.DATABASES.getDirectory();
if (dir != null) {
dir = dir.resolve("ExternalSources");
if (Files.isDirectory(dir)) {
final String[] resources = super.resources;
final String[] found = new String[resources.length - FIRST_FILE - 1];
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "EPSG_*.sql")) {
for (final Path path : stream) {
final String name = path.getFileName().toString();
for (int i=0; i<found.length; i++) {
final String part = resources[FIRST_FILE + i];
if (name.contains(part)) {
if (found[i] != null) {
log(Errors.getResources(locale)
.getLogRecord(Level.WARNING, Errors.Keys.DuplicatedFileReference_1, part));
return; // Stop the search because of duplicated file.
}
found[i] = name;
}
}
}
}
for (int i=0; i<found.length; i++) {
final String file = found[i];
if (file != null) {
resources[FIRST_FILE + i] = file;
} else {
dir = null;
}
}
directory = dir;
}
}
}
/**
* Returns {@code "EPSG"} if the scripts exist in the {@code ExternalSources} subdirectory,
* or {@code "unavailable"} otherwise.
*
* @return {@code "EPSG"} if the SQL scripts for installing the EPSG dataset are available,
* or {@code "unavailable"} otherwise.
*/
@Override
public Set<String> getAuthorities() {
return (directory != null) ? super.getAuthorities() : Collections.emptySet();
}
/**
* Returns {@code null} since the user is presumed to have downloaded the files himself.
*
* @return the terms of use in plain text or HTML, or {@code null} if the license is presumed already accepted.
*/
@Override
public String getLicense(String authority, Locale locale, String mimeType) {
return null;
}
/**
* Opens the input stream for the SQL script of the given name.
*
* @param name name of the script file to open.
* @return an input stream opened of the given script file, or {@code null} if the resource was not found.
* @throws IOException if an error occurred while opening the file.
*/
@Override
protected InputStream openStream(final String name) throws IOException {
return (directory != null) ? Files.newInputStream(directory.resolve(name)) : null;
}
}
}