/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2005-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2007-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.coverage.sql; import java.io.File; import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.PreparedStatement; import java.sql.SQLNonTransientException; import java.util.Set; import java.util.Map; import java.util.HashMap; import java.util.Objects; import java.util.logging.Level; import java.util.logging.LogRecord; import org.geotoolkit.resources.Errors; import org.geotoolkit.internal.sql.table.CatalogException; import org.geotoolkit.internal.sql.table.ConfigurationKey; import org.geotoolkit.internal.sql.table.Database; import org.geotoolkit.internal.sql.table.LocalCache; import org.geotoolkit.internal.sql.table.QueryType; import org.geotoolkit.internal.sql.table.SingletonTable; import org.geotoolkit.internal.sql.table.DuplicatedRecordException; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; import static org.apache.sis.util.collection.Containers.hashMapCapacity; /** * Connection to a table of series. This connection is used internally by the * {@linkplain LayerTable layer table}. * * @author Martin Desruisseaux (IRD, Geomatys) * @author Cédric Briançon (Geomatys) * @version 3.15 * * @since 3.10 (derived from Seagis) * @module */ final class SeriesTable extends SingletonTable<SeriesEntry> { /** * The format table, created when first needed. */ private transient FormatTable formats; /** * The layer for which we want the series. */ private String layer; /** * Creates a series table. * * @param database Connection to the database. */ public SeriesTable(final Database database) { this(new SeriesQuery(database)); } /** * Creates a series table using the specified query. */ private SeriesTable(final SeriesQuery query) { super(query, query.byIdentifier); } /** * Creates a new instance having the same configuration than the given table. * This is a copy constructor used for obtaining a new instance to be used * concurrently with the original instance. * * @param table The table to use as a template. */ private SeriesTable(final SeriesTable table) { super(table); } /** * Returns a copy of this table. This is a copy constructor used for obtaining * a new instance to be used concurrently with the original instance. */ @Override protected SeriesTable clone() { return new SeriesTable(this); } /** * Returns the layer for the series to be returned by {@link #getEntries() getEntries()}. * The default value is {@code null}, which means that no filtering should be performed. */ public String getLayer() { return layer; } /** * Sets the layer for the series to be returned. Next call to {@link #getEntries()} * will filters the series in order to return only the ones in this layer. */ public void setLayer(final String layer) { if (!Objects.equals(layer, this.layer)) { this.layer = layer; fireStateChanged("layer"); } } /** * Returns the {@link FormatTable} instance, creating it if needed. */ private FormatTable getFormatTable() throws CatalogException { FormatTable table = formats; if (table == null) { formats = table = getDatabase().getTable(FormatTable.class); } return table; } /** * Invoked automatically for a newly created statement or when this table * changed its state. The current implementation setups the SQL parameter * for the {@linkplain #getLayer currently selected layer}. */ @Override protected void configure(final LocalCache lc, final QueryType type, final PreparedStatement statement) throws SQLException { super.configure(lc, type, statement); final SeriesQuery query = (SeriesQuery) super.query; final int index = query.byLayer.indexOf(type); if (index != 0) { final String layer = getLayer(); if (layer == null) { throw new CatalogException(errors().getString(Errors.Keys.NoParameter_1, "layer")); } statement.setString(index, layer); } } /** * Creates a series from the current row in the specified result set. * * @param lc The {@link #getLocalCache()} value. * @param results The result set to read. * @param identifier The identifier of the series to create. * @return The entry for current row in the specified result set. * @throws SQLException if an error occurred while reading the database. */ @Override protected SeriesEntry createEntry(final LocalCache lc, final ResultSet results, final Comparable<?> identifier) throws SQLException { final SeriesQuery query = (SeriesQuery) super.query; final String formatID = results.getString(indexOf(query.format)); final String pathname = results.getString(indexOf(query.pathname)); final String extension = results.getString(indexOf(query.extension)); final String remarks = results.getString(indexOf(query.comments)); final String rootDirectory = getProperty(ConfigurationKey.ROOT_DIRECTORY); final String rootURL = getProperty(ConfigurationKey.ROOT_URL); final FormatEntry format = getFormatTable().getEntry(formatID); return new SeriesEntry((Integer) identifier, (rootDirectory != null) ? rootDirectory : rootURL, pathname, extension, format, remarks); } /** * Returns all series as (<var>identifier</var>, <var>series</var>) pairs. */ public Map<Integer,SeriesEntry> getEntriesMap() throws SQLException { final Set<SeriesEntry> entries = getEntries(); final Map<Integer,SeriesEntry> map = new HashMap<>(hashMapCapacity(entries.size())); for (final SeriesEntry entry : entries) { final Integer identifier = entry.getIdentifier(); if (map.put(identifier, entry) != null) { throw new DuplicatedRecordException(errors().getString(Errors.Keys.DuplicatedRecord_1, identifier)); } } return map; } /** * Returns the identifier for a series having the specified properties. * If no matching record is found, then this method returns {@code null}. * <p> * The {@link #setLayer(LayerEntry)} method must be invoked before this one. * * @param path The path relative to the root directory, or the base URL. * @param extension The extension to add to filenames. * @param format The format for the series considered. * @return The identifier of a matching entry, or {@code null} if none. * @throws SQLException if an error occurred while reading from the database. */ Integer find(final String path, final String extension, final String format) throws SQLException { ensureNonNull("path", path); ensureNonNull("extension", extension); ensureNonNull("format", format); Integer id = null; final SeriesQuery query = (SeriesQuery) super.query; final LocalCache lc = getLocalCache(); synchronized (lc) { final LocalCache.Stmt ce = getStatement(lc, QueryType.LIST); final PreparedStatement statement = ce.statement; final int idIndex = indexOf(query.identifier); final int pnIndex = indexOf(query.pathname); final int exIndex = indexOf(query.extension); final int ftIndex = indexOf(query.format); try (ResultSet results = statement.executeQuery()) { while (results.next()) { final int nextID = results.getInt(idIndex); String value = results.getString(pnIndex); if (value == null || !comparePaths(value, path)) { continue; } value = results.getString(exIndex); if (value == null || !value.equals(extension)) { continue; } value = results.getString(ftIndex); if (value == null || !value.equals(format)) { continue; } if (id != null && id.intValue() != nextID) { // Could happen if there is insufficient conditions in the WHERE clause. log("find", errors().getLogRecord(Level.WARNING, Errors.Keys.DuplicatedRecord_1, id)); continue; } id = nextID; } } release(lc, ce); } return id; } /** * Returns the identifier for a series having the specified properties. If no * matching record is found, then a new one is created and added to the database. * <p> * The {@link #setLayer(LayerEntry)} method must be invoked before this one. * * @param path The path relative to the root directory, or the base URL. * @param extension The extension to add to filenames. * @param format The format for the series considered. * @return The identifier of a matching entry (never {@code null}). * @throws SQLException if an error occurred while reading from or writing to the database. */ int findOrCreate(final String path, final String extension, final String format) throws SQLException { Integer id; final LocalCache lc = getLocalCache(); synchronized (lc) { boolean success = false; transactionBegin(lc); try { id = find(path, extension, format); if (id == null) { /* * No match found. Adds a new record in the database. */ final SeriesQuery query = (SeriesQuery) super.query; final LocalCache.Stmt ce = getStatement(lc, QueryType.INSERT); final PreparedStatement statement = ce.statement; statement.setString(indexOf(query.layer), getLayer()); statement.setString(indexOf(query.pathname), trimRoot(path)); statement.setString(indexOf(query.extension), extension); statement.setString(indexOf(query.format), format); success = updateSingleton(statement); try (ResultSet keys = statement.getGeneratedKeys()) { while (keys.next()) { id = keys.getInt(query.identifier.name); if (!keys.wasNull()) break; id = null; // Should never reach this point, but I'm paranoiac. } } release(lc, ce); } } finally { transactionEnd(lc, success); } } if (id == null) { // Should never occur, but I'm paranoiac. throw new SQLNonTransientException(); } return id; } /** * Returns {@code true} if the given paths are equals or equivalent. The two paths can * be relative or absolute, or only one path can be relative and the other one absolute. * * @param candidate The first path to compare. Can be relative or absolute. * @param path The second path to compare. Can be relative or absolute. * @return {@code true} if the two paths reference the same file. */ private boolean comparePaths(final String candidate, final String path) { if (candidate.equals(path)) { return true; } File candidateFile = new File(candidate); File pathFile = new File(path); if (candidateFile.equals(pathFile)) { return true; } if (candidateFile.isAbsolute() && !pathFile.isAbsolute()) { return compareRelativeAndAbsolutePaths(pathFile, candidateFile); } if (!candidateFile.isAbsolute() && pathFile.isAbsolute()) { return compareRelativeAndAbsolutePaths(candidateFile, pathFile); } /* * If the above failed, tries to compare absolute path. */ final String root = getProperty(ConfigurationKey.ROOT_DIRECTORY); if (root != null) { if (!candidateFile.isAbsolute()) { candidateFile = new File(root, candidateFile.getPath()); } if (!pathFile.isAbsolute()) { pathFile = new File(root, pathFile.getPath()); } } File cf = null; // Used for error message only. try { candidateFile = (cf = candidateFile).getCanonicalFile(); pathFile = (cf = pathFile).getCanonicalFile(); } catch (IOException exeption) { // Logs with a FINE level rather than WARNING because this exception may be normal. final LogRecord record = errors().getLogRecord(Level.FINE, Errors.Keys.NotADirectory_1, cf); record.setThrown(exeption); log("comparePaths", record); return false; } return candidateFile.equals(pathFile); } /** * Returns {@code true} if the given absolute path ends with the given relative path. * * @param relative The relative path. * @param absolute The absolute path. * @return {@code true} if the absolute path ends with the relative path. */ private static boolean compareRelativeAndAbsolutePaths(File relative, File absolute) { assert !relative.isAbsolute() : relative; assert absolute.isAbsolute() : absolute; do { if (!relative.getName().equals(absolute.getName())) { return false; } absolute = absolute.getParentFile(); if (absolute == null) { return false; } } while ((relative = relative.getParentFile()) != null); return true; } /** * Trims the root directory (if any) from the given path. */ private String trimRoot(String path) { String root = getProperty(ConfigurationKey.ROOT_DIRECTORY); if (root != null) { final File pathFile = new File(path); if (pathFile.isAbsolute()) { final File rootFile = new File(root); if (rootFile.isAbsolute()) { path = pathFile.getPath(); // For making sure that we use the right name separator. root = rootFile.getPath(); if (path.startsWith(root)) { path = path.substring(root.length()); if (path.startsWith(File.separator)) { path = path.substring(File.separator.length()); } } } } } return path.replace(File.separatorChar, '/').trim(); } }