/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2007-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-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.image.io.mosaic;
import java.io.Closeable;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Collections;
import java.util.Iterator;
import java.io.IOException;
import java.util.Locale;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import org.apache.sis.util.Disposable;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.Classes;
import org.geotoolkit.nio.IOUtilities;
import static org.geotoolkit.image.io.mosaic.Tile.LOGGER;
/**
* Cache the {@link ImageReader} instances used by {@link MosaicImageReader}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.18
*
* @since 3.18 (derived from 2.5)
* @module
*/
final class TileReaderPool implements Closeable, Disposable{
/**
* The locale to be given to the readers created by {@link #createReaderInstance(ImageReaderSpi)}.
*/
private Locale locale;
/**
* An unmodifiable view of {@link #readers} keys.
*
* @see #setProviders(Set)
*/
final Set<ImageReaderSpi> providers;
/**
* The image reader created for each provider. Keys must be the union of
* {@link TileManager#getImageReaderSpis} at every image index and must be computed right at
* {@link MosaicImageReader#setInput} invocation time. Values will be initially {@code null}
* - a reader will be assigned only when first needed.
*/
private final Map<ImageReaderSpi,ImageReader> readers;
/**
* The input given to each image reader. Values are {@linkplain Tile#getInput tile input}
* before they have been wrapped in an {@linkplain ImageInputStream image input stream}.
*
* @see MosaicImageReadParam#readers
*/
private final Map<ImageReader,Object> readerInputs;
/**
* Creates a new, initially empty, cache.
*/
TileReaderPool() {
readers = new HashMap<>();
readerInputs = new IdentityHashMap<>();
providers = Collections.unmodifiableSet(readers.keySet());
}
/**
* Sets the {@linkplain #providers}.
*
* @param providers The new set of providers.
*/
final void setProviders(final Set<ImageReaderSpi> providers) {
/*
* Closes the ImageReader that are no longuer in use given the new providers.
*/
final Iterator<Map.Entry<ImageReaderSpi,ImageReader>> it = readers.entrySet().iterator();
while (it.hasNext()) {
final Map.Entry<ImageReaderSpi,ImageReader> entry = it.next();
if (!providers.contains(entry.getKey())) {
final ImageReader reader = entry.getValue();
if (reader != null) {
/*
* Closes previous streams, if any. It is not a big deal if this operation
* fails, since we will not use anymore the old streams anyway. However it
* is worth to log.
*/
final Object rawInput = readerInputs.remove(reader);
final Object tileInput = reader.getInput();
if (rawInput != tileInput) try {
IOUtilities.close(tileInput);
} catch (IOException exception) {
Logging.unexpectedException(LOGGER, TileReaderPool.class, "setInput", exception);
}
reader.dispose();
}
it.remove();
}
}
/*
* Adds entries for the new providers that did not existed previously.
*/
for (final ImageReaderSpi provider : providers) {
if (!readers.containsKey(provider)) {
readers.put(provider, null);
}
}
assert providers.equals(this.providers);
assert readers.values().containsAll(readerInputs.keySet());
}
/**
* Creates a new {@link ImageReader} from the specified provider. This method do not
* check the cache and do not store the result in the cache. It should be invoked by
* {@link #getTileReader} and {@link #getTileReaders} methods only.
* <p>
* It is technically possible to return the same {@link ImageReader} instance from
* different {@link ImageReaderSpi}. It would broke the usual {@code ImageReaderSpi}
* contract for no obvious reason, but technically this class should work correctly
* even in such case.
*
* @param provider The provider. Must be a member of {@link #getTileReaderSpis}.
* @return The image reader for the given provider.
* @throws IOException if the image reader can not be created.
*/
private ImageReader createReaderInstance(final ImageReaderSpi provider) throws IOException {
final ImageReader reader = provider.createReaderInstance();
if (locale != null) {
try {
reader.setLocale(locale);
} catch (IllegalArgumentException e) {
// Invalid locale. Ignore this exception since it will not prevent the image
// reader to work mostly as expected (warning messages may be in a different
// locale, which is not a big deal).
Logging.recoverableException(LOGGER, TileReaderPool.class, "getTileReader", e);
}
}
return reader;
}
/**
* Returns the image reader for the given provider.
*
* @param provider The provider. Must be a member of {@link #getTileReaderSpis}.
* @return The image reader for the given provider.
* @throws IOException if the image reader can not be created.
*/
final ImageReader getTileReader(final ImageReaderSpi provider) throws IOException {
assert readers.containsKey(provider); // Key should exists even if the value is null.
ImageReader reader = readers.get(provider);
if (reader == null) {
reader = createReaderInstance(provider);
readers.put(provider, reader);
}
return reader;
}
/**
* Returns every readers used for reading tiles. New readers may be created on the fly
* by this method. However failure to create them will be logged rather than trown as
* an exception. In such case the information obtained by the caller may be incomplete
* and the exception may be thrown later when {@link #getTileReader} will be invoked.
*/
final Set<ImageReader> getTileReaders() {
for (final Map.Entry<ImageReaderSpi,ImageReader> entry : readers.entrySet()) {
ImageReader reader = entry.getValue();
if (reader == null) {
final ImageReaderSpi provider = entry.getKey();
try {
reader = createReaderInstance(provider);
} catch (IOException exception) {
Logging.unexpectedException(LOGGER, TileReaderPool.class, "getTileReaders", exception);
continue;
}
entry.setValue(reader);
}
if (!readerInputs.containsKey(reader)) {
readerInputs.put(reader, null);
}
}
assert readers.values().containsAll(readerInputs.keySet());
return readerInputs.keySet();
}
/**
* Returns a reader for the tiles, or {@code null}. This method tries to returns an instance
* of the most specific reader class. If no suitable instance is found, then it returns
* {@code null}.
* <p>
* This method is typically invoked for fetching an instance of {@code ImageReadParam}. We
* look for the most specific class because it may contains additional parameters that are
* ignored by super-classes. If we fail to find a suitable instance, then the caller shall
* fallback on the {@link ImageReader} default implementation.
*/
final ImageReader getTileReader() {
final Set<ImageReader> readers = getTileReaders();
Class<?> type = Classes.findSpecializedClass(readers);
while (type!=null && ImageReader.class.isAssignableFrom(type)) {
for (final ImageReader candidate : readers) {
if (type.equals(candidate.getClass())) {
return candidate;
}
}
type = type.getSuperclass();
}
return null;
}
/**
* Sets the current locale of image readers in this cache.
*
* @param locale The desired locale, or {@code null}.
*/
public void setLocale(final Locale locale) throws IllegalArgumentException {
this.locale = locale;
for (final ImageReader reader : readers.values()) {
try {
reader.setLocale(locale);
} catch (IllegalArgumentException e) {
// Locale not supported by the reader. It may occurs
// if not all readers support the same set of locales.
Logging.recoverableException(LOGGER, TileReaderPool.class, "setLocale", e);
}
}
}
/**
* Returns the raw input (<strong>not</strong> wrapped in an image input stream) for the
* given reader. This method is invoked by {@link Tile#getImageReader} only.
*/
final Object getRawInput(final ImageReader reader) {
return readerInputs.get(reader);
}
/**
* Sets the raw input (<strong>not</strong> wrapped in an image input stream) for the
* given reader. The input can be set to {@code null}. This method is invoked by
* {@link Tile#getImageReader} only.
*/
final void setRawInput(final ImageReader reader, final Object input) {
readerInputs.put(reader, input);
}
/**
* Closes any image input streams that may be held by tiles.
* The streams will be opened again when they will be first needed.
*
* @throws IOException if error occurred while closing a stream.
*/
@Override
public void close() throws IOException {
for (final Map.Entry<ImageReader,Object> entry : readerInputs.entrySet()) {
final ImageReader reader = entry.getKey();
final Object rawInput = entry.getValue();
final Object input = reader.getInput();
entry .setValue(null);
reader.setInput(null);
if (input != rawInput) {
IOUtilities.close(input);
}
}
}
/**
* Allows any resources held by this cache to be released. This method disposes every
* {@linkplain Tile#getImageReader tile image readers}.
* <p>
* It is the caller responsibility to invoke {@link #close()} before this method.
*/
@Override
public void dispose() {
readerInputs.clear();
for (final ImageReader reader : readers.values()) {
if (reader != null) {
reader.dispose();
}
}
readers.clear();
}
}