/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2010-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2010-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.IOException;
import java.util.Locale;
import java.util.Date;
import java.util.Set;
import java.util.SortedSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.RunnableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.ArrayBlockingQueue;
import java.lang.ref.WeakReference;
import java.lang.ref.Reference;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterValueGroup;
import org.apache.sis.util.Localized;
import org.apache.sis.measure.MeasurementRange;
import org.apache.sis.parameter.ParameterBuilder;
import org.apache.sis.util.NullArgumentException;
import org.opengis.util.FactoryException;
import org.geotoolkit.util.DateRange;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.coverage.io.GridCoverageReader;
import org.geotoolkit.coverage.io.GridCoverageReadParam;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.image.palette.IIOListeners;
import org.geotoolkit.internal.Threads;
import org.geotoolkit.internal.io.Installation;
import org.geotoolkit.internal.sql.table.ConfigurationKey;
import org.geotoolkit.internal.sql.table.TablePool;
import org.geotoolkit.resources.Errors;
/**
* A connection to a collection of coverages declared in a SQL database.
* The connection to the database is specified by a {@link DataSource}.
* <p>
* Every query methods in this class are executed in a background thread. In order to get the result
* immediately, the {@link FutureQuery#result()} convenience method can be used as in the example below:
*
* {@preformat java
* CoverageDatabase database = ...; // Specify your database here.
* Layer myLayer = database.getLayer("Temperature").result();
* // Use the layer here...
* }
*
* However it is better to invoke {@code FutureQuery.result()} as late as possible,
* in order to have more work executed concurrently.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.20
*
* @since 3.10
* @module
*/
public class CoverageDatabase implements Localized {
/**
* Maximal amount of concurrent threads which can be running. Note that higher values are
* not necessarily better, since each thread will typically perform a lot of SQL or I/O
* operations and too many concurrency in such operations may decrease performance.
*/
private static final int MAXIMUM_THREADS = 8;
/**
* Maximal amount of tasks which may be pending in the queue. If a greater amount of
* tasks is requested, the caller thread will be blocked until the number of tasks
* go down to this amount.
*/
private static final int MAXIMUM_TASKS = 256;
/**
* The default instance. Created only when first requested.
*
* @see #getDefaultInstance()
*/
private static Reference<CoverageDatabase> instance;
/**
* A description of the parameters expected by the {@code CoverageDatabase} constructors.
* See the {@linkplain org.geotoolkit.coverage.sql package javadoc} for an overview of
* those parameters.
*
* @since 3.18
*/
public static final ParameterDescriptorGroup PARAMETERS;
static {
final ConfigurationKey[] keys = ConfigurationKey.values();
final ParameterDescriptor<?>[] param = new ParameterDescriptor<?>[keys.length];
for (int i=0; i<keys.length; i++) {
final ConfigurationKey key = keys[i];
param[i] = new ParameterBuilder()
.addName(key.key)
.setRequired(false)
.create(String.class, key.defaultValue);
}
PARAMETERS = new ParameterBuilder().addName("CoverageDatabase").createGroup(param);
}
/**
* The object which will manage the connections to the database.
* This field shall never be {@code null}.
*/
volatile TableFactory database;
/**
* The executor service to use for loading data in background. Concurrency is only one raison
* for our usage of executor here. The other raison is to limit the amount of JDBC resources
* to be allocated, since we use a different connection for different thread.
*/
private final Executor executor;
/**
* The listener list.
*/
private final List<CoverageDatabaseListener> listeners;
/**
* The listeners as an array, or {@code null} if it need to be recomputed.
* A new array will be created every time the listener list is changed. We
* iterate over an array instead than over the list in order to avoid to
* hold the synchronization lock during the iteration.
*/
private volatile CoverageDatabaseListener[] listenerArray;
/**
* Creates a new instance using the given properties. The properties shall contains at
* least an entry for the {@code "URL"} key. The value of this entry shall be a JDBC URL
* in the form of {@code "jdbc:postgresql://host/database"}.
* <p>
* See the {@linkplain org.geotoolkit.coverage.sql package javadoc} for the list of
* parameters supported by this constructor.
*
* @param properties The configuration properties.
*
* @since 3.11
*/
public CoverageDatabase(final Properties properties) {
this(null, properties);
}
/**
* Creates a new instance using the given parameters. This constructor provides the same
* functionality than {@link #CoverageDatabase(Properties)}, but using the ISO 19111
* parameters construct instead than the Java properties.
* <p>
* See the {@linkplain org.geotoolkit.coverage.sql package javadoc} for the list of
* parameters supported by this constructor.
*
* @param parameters The configuration parameters.
*
* @since 3.18
*/
public CoverageDatabase(final ParameterValueGroup parameters) {
this(null, parameters);
}
/**
* Creates a new instance using the given data source and configuration properties.
* See the {@linkplain org.geotoolkit.coverage.sql package javadoc} for the list of
* parameters supported by this constructor.
*
* @param datasource The data source, or {@code null} for creating it from the URL.
* @param properties The configuration properties, or {@code null} if none.
*/
public CoverageDatabase(final DataSource datasource, final Properties properties) {
this(new TableFactory(datasource, properties));
}
/**
* Creates a new instance using the given data source and configuration parameters.
* See the {@linkplain org.geotoolkit.coverage.sql package javadoc} for the list of
* parameters supported by this constructor.
* <p>
* This constructor provides the same functionality than
* {@link #CoverageDatabase(DataSource, Properties)}, but using the ISO 19111
* parameters construct instead than the Java properties.
*
* @param datasource The data source, or {@code null} for creating it from the URL.
* @param parameters The configuration parameters, or {@code null} if none.
*
* @since 3.18
*/
public CoverageDatabase(final DataSource datasource, final ParameterValueGroup parameters) {
this(datasource, singleton(parameters));
}
/**
* Work around for RFE #4093999 in Sun's bug database
* ("Relax constraint on placement of this()/super() call in constructors").
*/
private static Properties singleton(final ParameterValueGroup parameters) {
if (parameters == null) {
return null;
}
final Properties properties = new Properties();
properties.put(ConfigurationKey.PARAMETERS, parameters);
return properties;
}
/**
* Creates a new instance using the given database.
*/
CoverageDatabase(final TableFactory db) {
database = db;
executor = new Executor();
listeners = new ArrayList<>(4);
}
/**
* Returns the default instance, or {@code null} if none. The default instance can be specified by
* the <a href="http://www.geotoolkit.org/modules/utility/geotk-setup/index.html">geotk-setup</a>
* module.
*
* @return The default instance, or {@code null} if none.
* @throws CoverageStoreException If an error occurred while fetching the default instance.
*
* @since 3.11
*/
public static synchronized CoverageDatabase getDefaultInstance() throws CoverageStoreException {
if (instance != null) {
final CoverageDatabase database = instance.get();
if (database != null) {
return database;
}
instance = null;
}
final Properties properties;
try {
properties = Installation.COVERAGES.getDataSource();
} catch (IOException e) {
throw new CoverageStoreException(e);
}
if (properties != null) {
final CoverageDatabase database = new CoverageDatabase(properties) {
@Override public void dispose() {
synchronized (CoverageDatabase.class) {
instance = null;
}
super.dispose();
}
};
instance = new WeakReference<>(database);
return database;
}
return null;
}
/**
* Returns the CRS authority factory used by this database. This factory is typically backed
* by the PostGIS {@code "spatial_ref_sys"} table - this is usually <strong>not</strong> the
* standard EPSG factory used by default in the Geotk library. In particular, axis order are
* often different.
*
* @return The CRS authority factory used by this database.
* @throws FactoryException If the factory can not be created.
*
* @since 3.12
*/
public CRSAuthorityFactory getCRSAuthorityFactory() throws FactoryException {
return database.getCRSAuthorityFactory();
}
/**
* Returns the Coordinate Reference System used by the database for indexing the coverages
* envelopes. This is the "native" CRS in which this {@code CoverageDatabase} instance will
* transform the requested envelopes before to execute the queries.
*
* @return The "native" coordinate reference system.
*
* @since 3.11
*/
public CoordinateReferenceSystem getCoordinateReferenceSystem() {
return database.spatioTemporalCRS;
}
/**
* The executor used by {@link CoverageDatabase}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.17
*
* @since 3.11
* @module
*/
private static final class Executor extends ThreadPoolExecutor {
/**
* Creates a new executor.
*/
Executor() {
super(0, MAXIMUM_THREADS, 1, TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(MAXIMUM_TASKS, true),
Threads.createThreadFactory("CoverageDatabase #"));
}
/**
* Returns the {@link FutureQueryTask} for the given callable task.
*/
@Override
protected <T> RunnableFuture<T> newTaskFor(final Callable<T> task) {
return new FutureQueryTask<>(task);
}
/**
* Executes the given task and casts the result to {@link FutureQuery},
* which is the expected type.
*/
@Override
public <T> FutureQuery<T> submit(final Callable<T> task) {
return (FutureQuery<T>) super.submit(task);
}
}
/**
* Ensures that the given argument is non-null.
*
* @param name The argument name.
* @param value The argument value.
* @throws IllegalArgumentException if the given value is {@code null}.
*/
private void ensureNonNull(final String name, final Object value) {
if (value == null) {
throw new NullArgumentException(Errors.getResources(getLocale())
.getString(Errors.Keys.NullArgument_1, name));
}
}
/**
* Returns the name of every layers is the database.
*
* @return The layer of the given name.
*/
public FutureQuery<Set<String>> getLayers() {
return executor.submit(new GetLayers());
}
/**
* The task for {@link CoverageDatabase#getLayers()}. Declared as an explicit class
* rather than an inner class in order to have more helpful stack trace in case of failure.
*/
private final class GetLayers implements Callable<Set<String>> {
/** Creates a new task. */
GetLayers() {
}
/** Executes the task in a background thread. */
@Override public Set<String> call() throws CoverageStoreException {
final TablePool<LayerTable> pool = database.layers;
final Set<String> names;
try {
final LayerTable table = pool.acquire();
names = table.getIdentifiers();
pool.release(table);
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
return names;
}
}
/**
* Returns the layer of the given name.
*
* @param name The layer name.
* @return The layer of the given name.
*/
public FutureQuery<Layer> getLayer(final String name) {
ensureNonNull("name", name);
return executor.submit(new GetLayer(name));
}
/**
* The task for {@link CoverageDatabase#getLayer(String)}. Declared as an explicit class
* rather than an inner class in order to have more helpful stack trace in case of failure.
*/
private final class GetLayer implements Callable<Layer> {
private final String name;
/** Creates a new task. */
GetLayer(final String name) {
this.name = name;
}
/** Executes the task in a background thread. */
@Override public Layer call() throws CoverageStoreException {
final TablePool<LayerTable> pool = database.layers;
final LayerEntry layer;
try {
final LayerTable table = pool.acquire();
layer = table.getEntry(name);
pool.release(table);
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
layer.setCoverageDatabase(CoverageDatabase.this);
return layer;
}
}
/**
* Adds a new layer of the given name, if it does not already exist. If a layer of the
* given name already exists, then this method does nothing and returns {@code false}.
*
* @param name The name of the new layer.
* @return {@code true} if the layer has been added, of {@code false} if a layer of
* the given name already exists.
*
* @since 3.11
*/
public FutureQuery<Boolean> addLayer(final String name) {
ensureNonNull("name", name);
return executor.submit(new AddLayer(name));
}
/**
* The task for {@link CoverageDatabase#addLayer(String)}. Declared as an explicit class
* rather than an inner class in order to have more helpful stack trace in case of failure.
*/
private final class AddLayer implements Callable<Boolean> {
private final String name;
/** Creates a new task. */
AddLayer(final String name) {
this.name = name;
}
/** Executes the task in a background thread. */
@Override public Boolean call() throws CoverageStoreException {
fireChange(true, +1, name);
final TablePool<LayerTable> pool = database.layers;
boolean added;
try {
final LayerTable table = pool.acquire();
added = table.createIfAbsent(name);
pool.release(table);
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
fireChange(false, added ? 1 : 0, name);
return added;
}
}
/**
* Removes the layer of the given name, if it exist. If no layer of the
* given name exists, then this method does nothing and returns {@code false}.
* <p>
* <strong>This action removes all references to raster data declared in that layer</strong>,
* unless the foreigner key constraints in the database have been changed from their default
* values. Note that the raster files are never deleted by this method.
*
* @param name The name of the layer to remove.
* @return {@code true} if the layer has been removed, of {@code false} if no layer of
* the given name exists.
*
* @since 3.11
*/
public FutureQuery<Boolean> removeLayer(final String name) {
ensureNonNull("name", name);
return executor.submit(new RemoveLayer(name));
}
/**
* The task for {@link CoverageDatabase#removeLayer(String)}. Declared as an explicit class
* rather than an inner class in order to have more helpful stack trace in case of failure.
*/
private final class RemoveLayer implements Callable<Boolean> {
private final String name;
/** Creates a new task. */
RemoveLayer(final String name) {
this.name = name;
}
/** Executes the task in a background thread. */
@Override public Boolean call() throws CoverageStoreException {
fireChange(true, -1, name);
final TablePool<LayerTable> pool = database.layers;
int removed;
try {
final LayerTable table = pool.acquire();
removed = table.delete(name);
pool.release(table);
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
fireChange(false, -removed, name);
return (removed != 0);
}
}
/**
* Returns a time range encompassing all coverages in this layer.
* This method is equivalent to the code below, except that more
* code are executed in the background thread:
*
* {@preformat java
* return getLayer(layer).result().getTimeRange();
* }
*
* @param layer The layer for which the time range is desired.
* @return The time range encompassing all coverages.
*
* @see Layer#getTimeRange()
*/
public FutureQuery<DateRange> getTimeRange(final String layer) {
ensureNonNull("layer", layer);
return executor.submit(new GetTimeRange(layer));
}
/**
* The task for {@link CoverageDatabase#getTimeRange(String)}. Declared as an explicit class
* rather than an inner class in order to have more helpful stack trace in case of failure.
*/
private final class GetTimeRange implements Callable<DateRange> {
private final String layer;
/** Creates a new task. */
GetTimeRange(final String layer) {
this.layer = layer;
}
/** Executes the task in a background thread. */
@Override public DateRange call() throws CoverageStoreException {
final TablePool<LayerTable> pool = database.layers;
final Layer entry;
try {
final LayerTable table = pool.acquire();
entry = table.getEntry(layer);
pool.release(table);
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
return entry.getTimeRange();
}
}
/**
* Returns the set of dates when a coverage is available.
* This method is equivalent to the code below, except that
* more code are executed in the background thread:
*
* {@preformat java
* return getLayer(layer).result().getAvailableTimes();
* }
*
* @param layer The layer for which the available times are desired.
* @return The set of dates.
*
* @see Layer#getAvailableTimes()
*/
public FutureQuery<SortedSet<Date>> getAvailableTimes(final String layer) {
ensureNonNull("layer", layer);
return executor.submit(new GetAvailableTimes(layer));
}
/**
* The task for {@link CoverageDatabase#getAvailableTimes(String)}. Declared as an explicit class
* rather than an inner class in order to have more helpful stack trace in case of failure.
*/
private final class GetAvailableTimes implements Callable<SortedSet<Date>> {
private final String layer;
/** Creates a new task. */
GetAvailableTimes(final String layer) {
this.layer = layer;
}
/** Executes the task in a background thread. */
@Override public SortedSet<Date> call() throws CoverageStoreException {
final TablePool<LayerTable> pool = database.layers;
final Layer entry;
try {
final LayerTable table = pool.acquire();
entry = table.getEntry(layer);
pool.release(table);
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
return entry.getAvailableTimes();
}
}
/**
* Returns the set of altitudes where a coverage is available.
* This method is equivalent to the code below, except that
* more code are executed in the background thread:
*
* {@preformat java
* return getLayer(layer).result().getAvailableElevations();
* }
*
* @param layer The layer for which the available elevations are desired.
* @return The set of altitudes.
*
* @see Layer#getAvailableElevations()
*/
public FutureQuery<SortedSet<Number>> getAvailableElevations(final String layer) {
ensureNonNull("layer", layer);
return executor.submit(new GetAvailableElevations(layer));
}
/**
* The task for {@link CoverageDatabase#getAvailableElevations(String)}. Declared as an explicit
* class rather than an inner class in order to have more helpful stack trace in case of failure.
*/
private final class GetAvailableElevations implements Callable<SortedSet<Number>> {
private final String layer;
/** Creates a new task. */
GetAvailableElevations(final String layer) {
this.layer = layer;
}
/** Executes the task in a background thread. */
@Override public SortedSet<Number> call() throws CoverageStoreException {
final TablePool<LayerTable> pool = database.layers;
final Layer entry;
try {
final LayerTable table = pool.acquire();
entry = table.getEntry(layer);
pool.release(table);
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
return entry.getAvailableElevations();
}
}
/**
* Returns the ranges of valid <cite>geophysics</cite> values for each band of the given layer.
* This method is equivalent to the code below, except that more code are executed in the
* background thread:
*
* {@preformat java
* return getLayer(layer).result().getSampleValueRanges();
* }
*
* @param layer The layer for which the range of measurement values is desired.
* @return The range of valid sample values.
*
* @see Layer#getSampleValueRanges()
*/
public FutureQuery<List<MeasurementRange<?>>> getSampleValueRanges(final String layer) {
ensureNonNull("layer", layer);
return executor.submit(new GetSampleValueRanges(layer));
}
/**
* The task for {@link CoverageDatabase#getSampleValueRanges(String)}. Declared as an explicit
* class rather than an inner class in order to have more helpful stack trace in case of failure.
*/
private final class GetSampleValueRanges implements Callable<List<MeasurementRange<?>>> {
private final String layer;
/** Creates a new task. */
GetSampleValueRanges(final String layer) {
this.layer = layer;
}
/** Executes the task in a background thread. */
@Override public List<MeasurementRange<?>> call() throws CoverageStoreException {
final TablePool<LayerTable> pool = database.layers;
final Layer entry;
try {
final LayerTable table = pool.acquire();
entry = table.getEntry(layer);
pool.release(table);
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
return entry.getSampleValueRanges();
}
}
/**
* Reads the data of a two-dimensional slice and returns them as a coverage.
* Note that the returned two-dimensional slice is not guaranteed to have exactly
* the {@linkplain CoverageQuery#getEnvelope() requested envelope}. Callers may
* need to check the geometry of the returned envelope and perform an additional
* resampling if needed.
*
* @param layer The layer of the coverage to query.
* @param envelope The desired envelope and resolution, or {@code null} for all data.
* @param listeners The listeners, or {@code null} if none.
* @return The coverage.
*
* @see LayerCoverageReader#readSlice(int, GridCoverageReadParam)
*
* @since 3.20
*/
public FutureQuery<GridCoverage2D> readSlice(final String layer, final CoverageEnvelope envelope, final IIOListeners listeners) {
ensureNonNull("layer", layer);
return executor.submit(new ReadSlice(layer, envelope, listeners));
}
/**
* The task for {@link CoverageDatabase#readSlice(CoverageQuery, IIOListeners)}. Declared as an
* explicit class rather than an inner class in order to have more helpful stack trace in case
* of failure.
*/
private final class ReadSlice implements Callable<GridCoverage2D> {
private final String layer;
private final CoverageEnvelope envelope;
private final IIOListeners listeners;
/** Creates a new task. */
ReadSlice(final String layer, final CoverageEnvelope envelope, final IIOListeners listeners) {
this.layer = layer;
this.envelope = envelope;
this.listeners = listeners;
}
/** Executes the task in a background thread. */
@Override public GridCoverage2D call() throws CoverageStoreException {
final TablePool<GridCoverageTable> pool = database.coverages;
final GridCoverageReference entry;
try {
final GridCoverageTable table = pool.acquire();
table.setLayer(layer);
table.envelope.setAll(envelope);
entry = table.getEntry();
pool.release(table);
} catch (SQLException e) {
throw new CoverageStoreException(e);
} catch (TransformException e) {
throw new CoverageStoreException(Errors.format(
Errors.Keys.IllegalCoordinateReferenceSystem), e);
}
return entry.read(envelope, listeners);
}
}
/**
* Configures and returns a {@link GridCoverageReader} for the given layer. This provides an
* alternative way (as compared to {@link #readSlice readSlice}) for reading two-dimensional
* slices of coverage. This method is provided for inter-operability with libraries which want
* to access to the data through the {@link GridCoverageReader} API only.
*
* @param layer The name of the initial layer to be read by the returned reader, or {@code null}.
* @return A grid coverage reader using the given layer as input.
* @throws CoverageStoreException If an error occurred while querying the database.
*/
public LayerCoverageReader createGridCoverageReader(final String layer) throws CoverageStoreException {
final FutureQuery<Layer> future = (layer != null) ? getLayer(layer) : null;
return new LayerCoverageReader(this, future);
}
/**
* Configures and returns a {@link LayerCoverageWriter} for the given layer.
* This method is provided for inter-operability with libraries which want
* to add data through the {@link LayerCoverageWriter} API only.
*
* @param layer The name of the initial layer to be read by the returned writer, or {@code null}.
* @return A grid coverage writer using the given layer as input.
* @throws CoverageStoreException If an error occurred while querying the database.
*
* @since 3.20
*/
public LayerCoverageWriter createGridCoverageWriter(final String layer) throws CoverageStoreException {
final FutureQuery<Layer> future = (layer != null) ? getLayer(layer) : null;
return new LayerCoverageWriter(this, future);
}
/**
* Adds the given object to the list of objects to notify about changes in database content.
*
* @param listener The new listener to add.
*
* @since 3.12
*/
public void addListener(final CoverageDatabaseListener listener) {
if (listener != null) {
synchronized (listeners) {
if (!listeners.contains(listener)) {
if (listeners.add(listener)) {
listenerArray = null;
}
}
}
}
}
/**
* Removes the given object from the list of objects to notify about changes in database
* content. This method does nothing if the given object is not a member of the listener
* list.
*
* @param listener The listener to remove.
*
* @since 3.12
*/
public void removeListener(final CoverageDatabaseListener listener) {
synchronized (listeners) {
if (listeners.remove(listener)) {
listenerArray = null;
}
}
}
/**
* Returns all listeners to notify about changes in database constent, or an empty array
* if none. This method returns a direct reference to the internal array; <strong>do not
* modify</strong>.
*
* @since 3.12
*/
private CoverageDatabaseListener[] getInternalListeners() {
CoverageDatabaseListener[] array = listenerArray;
if (array == null) {
synchronized (listeners) {
array = listenerArray;
if (array == null) {
array = listeners.toArray(new CoverageDatabaseListener[listeners.size()]);
listenerArray = array;
}
}
}
return array;
}
/**
* Returns all listeners to notify about changes in database constent, or an empty array
* if none.
*
* @return All registered listeners, or an empty array if none.
*
* @since 3.12
*/
public CoverageDatabaseListener[] getListeners() {
return getInternalListeners().clone();
}
/**
* Notifies every listener that a value is about to change, or have already changed.
* The method to be invoked is determined from the type of the {@code value} argument,
* which can be {@link String} (for layers) or {@link NewGridCoverageReference}.
*
* @param isBefore {@code true} if the event is invoked before the change,
* or {@code false} if the event occurs after the change.
* @param numEntryChange Number of entries added, or a negative number if entries removed.
* @param value The entry which is added or removed.
* @throws DatabaseVetoException if {@code isBefore} is {@code true} and a listener vetoed
* against the change.
*/
final void fireChange(final boolean isBefore, final int numEntryChange, final Object value)
throws DatabaseVetoException
{
final CoverageDatabaseListener[] listeners = getInternalListeners();
if (listeners.length != 0) {
final CoverageDatabaseEvent event = new CoverageDatabaseEvent(this, isBefore, numEntryChange);
for (final CoverageDatabaseListener listener : listeners) {
try {
if (value instanceof NewGridCoverageReference) {
listener.coverageAdding(event, (NewGridCoverageReference) value);
} else {
listener.layerListChange(event, (String) value);
}
} catch (DatabaseVetoException veto) {
if (isBefore) {
throw veto;
}
final String method;
if (value instanceof NewGridCoverageReference) {
method = "coverageAdding";
} else {
method = "layerListChange";
}
final LogRecord record = new LogRecord(Level.WARNING,
Errors.getResources(getLocale()).getString(Errors.Keys.VetoTooLate));
record.setSourceClassName(CoverageDatabaseListener.class.getName());
record.setSourceMethodName(method);
record.setThrown(veto);
database.getLogger().log(record);
}
}
}
}
/**
* Returns the locale used for formatting logging and error messages.
*
* @return The locale, or {@code null} for the {@linkplain Locale#getDefault() default} locale.
*/
@Override
public Locale getLocale() {
return database.getLocale();
}
/**
* Flushes the cache. This method shall be invoked when the database content has been
* changed by some other way than through the {@code CoverageDatabase} API.
*/
public void flush() {
database = new TableFactory(database);
}
/**
* Disposes the resources used by this database.
*/
public void dispose() {
executor.shutdown();
}
/*
* No need to override finalize(), because ThreadPoolExecutor already
* has a finalize() method which invoke its shutdown() method.
*/
}