/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2007-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.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.SortedSet;
import java.util.Iterator;
import java.util.Calendar;
import java.util.Comparator;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.sql.Timestamp;
import java.sql.ResultSet;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.imageio.IIOException;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import org.geotoolkit.image.io.mosaic.Tile;
import org.geotoolkit.image.io.mosaic.TileManager;
import org.geotoolkit.image.io.mosaic.TileManagerFactory;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.collection.Cache;
import org.apache.sis.util.collection.BackingStoreException;
import org.geotoolkit.internal.sql.table.Table;
import org.geotoolkit.internal.sql.table.Database;
import org.geotoolkit.internal.sql.table.QueryType;
import org.geotoolkit.internal.sql.table.LocalCache;
import org.geotoolkit.internal.sql.table.SpatialDatabase;
import org.geotoolkit.internal.sql.table.CatalogException;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.resources.Errors;
import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
/**
* Connection to a table of {@linkplain Tiles tiles}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.15
*
* @since 3.10 (derived from Seagis)
* @module
*/
final class TileTable extends Table implements Comparator<TileManager> {
/**
* The table of grid geometries. Will be created only when first needed.
*/
private transient GridGeometryTable gridGeometryTable;
/**
* A cache of tile managers created up to date. This cache is shared by all
* instances of {@link TileTable} created from the same {@link SpatialDatabase}.
*/
private final Cache<CoverageRequest,TileManager[]> cache;
/**
* Creates a tile table.
*
* {@section Implementation note}
* This constructor actually expects an instance of {@link SpatialDatabase},
* but we have to keep {@link Database} in the method signature because this
* constructor is fetched by reflection.
*
* @param database The connection to the database.
*/
public TileTable(final Database database) {
super(new TileQuery((SpatialDatabase) database));
cache = new Cache<>();
}
/**
* 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 TileTable(final TileTable table) {
super(table);
cache = table.cache;
}
/**
* 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 TileTable clone() {
return new TileTable(this);
}
/**
* Returns the {@link GridGeometryTable} instance, creating it if needed.
*/
private GridGeometryTable getGridGeometryTable() throws CatalogException {
GridGeometryTable table = gridGeometryTable;
if (table == null) {
gridGeometryTable = table = getDatabase().getTable(GridGeometryTable.class);
}
return table;
}
/**
* Returns {@code true} if at least one tile exists for the given layer.
*
* @param layer The layer to test.
* @return {@code true} if a tile exists.
*/
public boolean exists(final LayerEntry layer) throws SQLException {
final TileQuery query = (TileQuery) this.query;
final boolean exists;
final LocalCache lc = getLocalCache();
synchronized (lc) {
final LocalCache.Stmt ce = getStatement(lc, QueryType.EXISTS);
final PreparedStatement statement = ce.statement;
statement.setString(indexOf(query.byLayer), layer.getName());
try (ResultSet results = statement.executeQuery()) {
exists = results.next();
}
release(lc, ce);
}
return exists;
}
/**
* Returns the tile manager for the given layer and date range. This method usually returns a
* single tile manager, but more could be returned if the tiles can not fit all in the same
* instance.
*
* @param layer The layer.
* @param startTime The start time, or {@code null} if none.
* @param endTime The end time, or {@code null} if none.
* @param srid The numeric identifier of the CRS.
* @return The tile managers for the given series and date range.
* @throws CatalogException if an inconsistent record is found in the database.
* @throws SQLException if an error occurred while reading the database.
*/
public TileManager[] getTiles(final LayerEntry layer, final Timestamp startTime,
final Timestamp endTime, final int srid) throws SQLException, IOException
{
final CoverageRequest request = new CoverageRequest(layer, startTime, endTime, srid);
TileManager[] managers = cache.peek(request);
if (managers == null) {
final Cache.Handler<TileManager[]> handler = cache.lock(request);
try {
managers = handler.peek();
if (managers == null) {
final File cacheFile = getCacheFile(layer);
managers = load(cacheFile);
if (managers == null) {
final TileQuery query = (TileQuery) this.query;
final List<Tile> tiles = new ArrayList<>();
final LocalCache lc = getLocalCache();
synchronized (lc) {
final Calendar calendar = getCalendar(lc);
final LocalCache.Stmt ce = getStatement(lc, QueryType.LIST);
final PreparedStatement statement = ce.statement;
statement.setString (indexOf(query.byLayer), layer.getName());
statement.setTimestamp(indexOf(query.byStartTime), startTime, calendar);
statement.setTimestamp(indexOf(query.byEndTime), endTime, calendar);
statement.setInt (indexOf(query.byHorizontalSRID), srid);
final int seriesIndex = indexOf(query.series);
final int filenameIndex = indexOf(query.filename);
final int indexIndex = indexOf(query.index);
final int extentIndex = indexOf(query.spatialExtent);
final int dxIndex = indexOf(query.dx);
final int dyIndex = indexOf(query.dy);
try (ResultSet results = statement.executeQuery()) {
SeriesEntry series = null;
ImageReaderSpi provider = null;
GridGeometryEntry geometry = null;
int lastSeriesID = 0;
int lastExtentID = 0;
while (results.next()) {
final int seriesID = results.getInt (seriesIndex);
final String filename = results.getString(filenameIndex);
final int index = results.getInt (indexIndex);
final int extent = results.getInt (extentIndex);
final int dx = results.getInt (dxIndex); // '0' if null, which is fine.
final int dy = results.getInt (dyIndex); // '0' if null, which is fine.
/*
* Gets the series, which usually never change for the whole mosaic (but this is not
* mandatory - the real thing that can't change is the layer). The series is needed
* in order to build the absolute pathname from the relative one.
*/
if (series == null || seriesID != lastSeriesID) {
// Computes only if the series changed. Usually it doesn't change.
series = layer.getSeries(seriesID);
provider = getImageReaderSpi(series.format.imageFormat);
lastSeriesID = seriesID;
}
Object input = series.file(filename);
if (!((File) input).isAbsolute()) try {
input = series.uri(filename);
} catch (URISyntaxException e) {
throw new IIOException(e.getLocalizedMessage(), e);
}
/*
* Gets the geometry, which usually don't change often. The same geometry can be shared
* by all tiles at the same level, given that the only change is the (dx,dy) translation
* term defined explicitly in the "Tiles" table. Doing so avoid the creation a thousands
* of new "GridGeometries" entries.
*/
if (geometry == null || extent != lastExtentID) {
geometry = getGridGeometryTable().getEntry(extent);
lastExtentID = extent;
}
AffineTransform gridToCRS = geometry.gridToCRS;
if (dx != 0 || dy != 0) {
gridToCRS = new AffineTransform(gridToCRS);
gridToCRS.translate(dx, dy);
}
final Rectangle bounds = geometry.getImageBounds();
final Tile tile = new Tile(provider, input, (index != 0) ? index-1 : 0, bounds, gridToCRS);
tiles.add(tile);
}
}
release(lc, ce);
}
/*
* Get the array of TileManager. The array should contains only one element.
* But if we get more element, put the TileManager having the greatest amount
* of tiles first because it is typically the one which will be used by the
* GridCoverageLoader.
*/
if (!tiles.isEmpty()) try {
managers = TileManagerFactory.DEFAULT.create(tiles);
Arrays.sort(managers, this);
} catch (BackingStoreException e) {
throw e.unwrapOrRethrow(IOException.class);
}
save(managers, cacheFile);
}
}
} finally {
handler.putAndUnlock(managers);
}
}
return managers;
}
/**
* The comparator used by the above {@link #getTiles} method for putting first the
* {@code TileManager} which have the greatest amount of tiles.
*
* @param o1 The first tile manager to compare.
* @param o2 The second tile manager to compare.
* @return A negative number if o1 has more times than o2.
* @throws BackingStoreException If an {@link IOException} occurred.
*/
@Override
public int compare(final TileManager o1, final TileManager o2) throws BackingStoreException {
try {
return o2.getTiles().size() - o1.getTiles().size();
} catch (IOException e) {
throw new BackingStoreException(e);
}
}
/**
* Returns an image reader for the specified name. The argument can be either a format
* name or a mime type.
*/
private static ImageReaderSpi getImageReaderSpi(final String format) throws IIOException {
final IIORegistry registry = IIORegistry.getDefaultInstance();
Iterator<ImageReaderSpi> providers = registry.getServiceProviders(ImageReaderSpi.class, true);
ImageReaderSpi fallback = null;
while (providers.hasNext()) {
final ImageReaderSpi provider = providers.next();
if (ArraysExt.containsIgnoreCase(provider.getFormatNames(), format)) {
if (!Tile.ignore(provider)) {
return provider;
}
if (fallback == null) {
fallback = provider;
}
}
}
/*
* Tests for MIME type only if no provider was found for the format name. We do not merge
* the check for MIME type in the above loop because it has a cost (getMIMETypes() clones
* an array) and should not be needed for database registering their format by name. This
* check is performed mostly for compatibility purpose with policy in previous versions.
*/
providers = registry.getServiceProviders(ImageReaderSpi.class, true);
while (providers.hasNext()) {
final ImageReaderSpi provider = providers.next();
if (ArraysExt.containsIgnoreCase(provider.getMIMETypes(), format)) {
if (!Tile.ignore(provider)) {
return provider;
}
if (fallback == null) {
fallback = provider;
}
}
}
if (fallback != null) {
return fallback;
}
throw new IIOException(Errors.format(Errors.Keys.NoImageReader));
}
/**
* Returns the file of the cached tile manager. If there is no known directory, then
* this method returns {@code null}.
*
* @return The {@value TileManager#SERIALIZED_FILENAME} file, or {@code null} if none.
* @throws SQLException If an error occurred which querying the database.
*
* @since 3.15
*/
private static File getCacheFile(final Layer layer) throws SQLException {
SortedSet<File> directories = null;
try {
directories = layer.getImageDirectories();
} catch (CoverageStoreException e) {
final Throwable cause = e.getCause();
if (cause instanceof SQLException) {
throw (SQLException) cause;
}
recoverableException(e);
}
if (!isNullOrEmpty(directories)) {
File file = directories.first();
file = new File(file, TileManager.SERIALIZED_FILENAME);
return file;
}
return null;
}
/**
* Tries to load the tile managers from the given file. If a {@value TileManager#SERIALIZED_FILENAME}
* file exists, loading it is much faster than creating it from the database content.
*
* @param file The file of the serialized tile managers, or {@code null} if none.
* @return The tile managers, or {@code null} if none.
* @throws IOException If an I/O error occurred, except corrupted stream
* (since the tile manager can be generated from the database content).
*
* @since 3.15
*/
private static TileManager[] load(final File file) throws IOException {
TileManager[] managers = null;
if (file != null) try {
if (file.isFile() && file.canRead()) {
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
managers = (TileManager[]) in.readObject();
}
}
} catch (ObjectStreamException | ClassNotFoundException | ClassCastException | SecurityException e) {
recoverableException(e);
}
return managers;
}
/**
* Saves the tile managers in the given file, for future reuse by the {@link #load} method.
* This method does nothing if there is no write permission for the file or its parent
* directory, or if the given file already exists.
*
* @param managers The tile managers to save, or {@code null} or an empty array if none.
* @param file The file where to save the tile managers, or {@code null} if none.
* @throws IOException If any I/O error occurred.
*
* @since 3.15
*/
private static void save(final TileManager[] managers, final File file) throws IOException {
if (file != null && managers != null && managers.length != 0) {
final File parent = file.getParentFile();
if (parent != null) try {
if (parent.isDirectory() && parent.canWrite() && file.createNewFile()) {
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
out.writeObject(managers);
}
}
} catch (SecurityException e) {
recoverableException(e);
}
}
}
/**
* Logs a message about an excepted, but recoverable, exception. The declared source method is
* {@code "getTiles"} since this is the public method which logged (indirectly) the exception.
*
* @since 3.15
*/
private static void recoverableException(final Exception e) {
Logging.recoverableException(null, TileTable.class, "getTiles", e);
}
}