blob: 6b041d655e9611da33497497ddb8336a83d8d6aa [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.storage.image;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.HashMap;
import java.util.logging.Level;
import java.io.Closeable;
import java.io.IOException;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.nio.file.NoSuchFileException;
import java.nio.file.StandardOpenOption;
import java.net.URISyntaxException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import org.opengis.metadata.Metadata;
import org.opengis.metadata.maintenance.ScopeCode;
import org.apache.sis.coverage.grid.PixelInCell;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.io.stream.IOUtilities;
import org.apache.sis.storage.Resource;
import org.apache.sis.storage.Aggregate;
import org.apache.sis.storage.StorageConnector;
import org.apache.sis.storage.GridCoverageResource;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreClosedException;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.ReadOnlyStorageException;
import org.apache.sis.storage.UnsupportedStorageException;
import org.apache.sis.storage.base.PRJDataStore;
import org.apache.sis.storage.base.MetadataBuilder;
import org.apache.sis.storage.base.AuxiliaryContent;
import org.apache.sis.referencing.privy.AffineTransform2D;
import org.apache.sis.metadata.sql.MetadataStoreException;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.privy.ListOfUnknownSize;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.setup.OptionKey;
/**
* A data store which creates grid coverages from Image I/O readers using <i>World File</i> convention.
* Georeferencing is defined by two auxiliary files having the same name as the image file but different suffixes:
*
* <ul class="verbose">
* <li>A text file containing the coefficients of the affine transform mapping pixel coordinates to geodesic coordinates.
* The reader expects one coefficient per line, in the same order as the order expected by the
* {@link java.awt.geom.AffineTransform#AffineTransform(double[]) AffineTransform(double[])} constructor, which is
* <var>scaleX</var>, <var>shearY</var>, <var>shearX</var>, <var>scaleY</var>, <var>translateX</var>, <var>translateY</var>.
* The reader looks for a file having the following suffixes, in preference order:
* <ol>
* <li>The first letter of the image file extension, followed by the last letter of
* the image file extension, followed by {@code 'w'}. Example: {@code "tfw"} for
* {@code "tiff"} images, and {@code "jgw"} for {@code "jpeg"} images.</li>
* <li>The extension of the image file with a {@code 'w'} appended.</li>
* <li>The {@code "wld"} extension.</li>
* </ol>
* </li>
* <li>A text file containing the <i>Coordinate Reference System</i> (CRS) definition
* in <i>Well Known Text</i> (WKT) syntax.
* The reader looks for a file having the {@code ".prj"} extension.</li>
* </ul>
*
* Every auxiliary text file are expected to be encoded in UTF-8
* and every numbers are expected to be formatted in US locale.
*
* <h2>Type of input objects</h2>
* The {@link StorageConnector} input should be an instance of the following types:
* {@link java.nio.file.Path}, {@link java.io.File}, {@link java.net.URL} or {@link java.net.URI}.
* Other types such as {@link ImageInputStream} are also accepted but in those cases the auxiliary files cannot be read.
* For any input of unknown type, this data store first checks if an {@link ImageReader} accepts the input type directly.
* If none is found, this data store tries to {@linkplain ImageIO#createImageInputStream(Object) create an input stream}
* from the input object.
*
* <p>The storage input object may also be an {@link ImageReader} instance ready for use
* (i.e. with its {@linkplain ImageReader#setInput(Object) input set} to a non-null value).
* In that case, this data store will use the given image reader as-is.
* The image reader will be {@linkplain ImageReader#dispose() disposed}
* and its input closed (if {@link AutoCloseable}) when this data store is {@linkplain #close() closed}.</p>
*
* <h2>Handling of multi-image files</h2>
* Because some image formats can store an arbitrary number of images,
* this data store is considered as an aggregate with one resource per image.
* All image should have the same size and all resources will share the same {@link GridGeometry}.
* However, this base class does not implement the {@link Aggregate} interface directly in order to
* give a chance to subclasses to implement {@link GridCoverageResource} directly when the format
* is known to support only one image per file.
*
* @author Martin Desruisseaux (Geomatys)
*/
public class WorldFileStore extends PRJDataStore {
/**
* Image I/O format names (ignoring case) for which we have an entry in the {@code SpatialMetadata} database.
*
* @see <a href="https://issues.apache.org/jira/browse/SIS-300">SIS-300 — Complete the information provided in Citations constants</a>
*/
private static final String[] KNOWN_FORMATS = {
"PNG"
};
/**
* Index of the main image. This is relevant only with formats capable to store an arbitrary number of images.
* Current implementation assumes that the main image is always the first one, but it may become configurable
* in a future version if useful.
*
* @see #width
* @see #height
*/
static final int MAIN_IMAGE = 0;
/**
* The default World File suffix when it cannot be determined from {@link #location}.
* This is a GDAL convention.
*/
private static final String DEFAULT_SUFFIX = "wld";
/**
* The "cell center" versus "cell corner" interpretation of translation coefficients.
* The ESRI specification said that the coefficients map to pixel center.
*/
static final PixelInCell CELL_ANCHOR = PixelInCell.CELL_CENTER;
/**
* The filename extension (may be an empty string), or {@code null} if unknown.
* It does not include the leading dot.
*/
final String suffix;
/**
* The filename extension for the auxiliary "world file".
* For the TIFF format, this is typically {@code "tfw"}.
* This is computed as a side-effect of {@link #readWorldFile()}.
*/
private String suffixWLD;
/**
* The image reader, set by the constructor and cleared when the store is closed.
* May also be null if the store is initially write-only, in which case a reader
* may be created the first time than an image is read.
*
* @see #reader()
*/
private volatile ImageReader reader;
/**
* The object to close when {@code WorldFileStore} is closed. It may be a different object than
* reader input or writer output, because some {@link ImageInputStream#close()} implementations
* in the standard Java {@link javax.imageio.stream} package do not close the underlying stream.
*
* <p>The type is {@link Closeable} instead of {@link AutoCloseable} because the former is idempotent:
* invoking {@link Closeable#close()} many times has no effect. By contrast {@link AutoCloseable} does
* not offer this guarantee. Because it is hard to know what {@link ImageInputStream#close()} will do,
* we need idempotent {@code toClose} for safety. Note that {@link ImageInputStream#close()} violates
* the idempotent contract of {@link Closeable#close()}, so an additional check will be necessary in
* our {@link #close()} implementation.</p>
*
* @see javax.imageio.stream.FileCacheImageInputStream#close()
* @see javax.imageio.stream.FileCacheImageOutputStream#close()
* @see javax.imageio.stream.MemoryCacheImageInputStream#close()
* @see javax.imageio.stream.MemoryCacheImageOutputStream#close()
*/
private Closeable toClose;
/**
* Width and height of the main image.
* The {@link #gridGeometry} is assumed valid only for images having this size.
*
* @see #MAIN_IMAGE
* @see #gridGeometry
*/
private int width, height;
/**
* The conversion from pixel center to CRS, or {@code null} if none or not yet computed.
* The grid extent has the size given by {@link #width} and {@link #height}.
*
* @see #crs
* @see #width
* @see #height
* @see #getGridGeometry(int)
*/
private GridGeometry gridGeometry;
/**
* All images in this resource, created when first needed.
* Elements in this list will also be created when first needed.
*
* @see #components()
*/
private Components components;
/**
* The metadata object, or {@code null} if not yet created.
*
* @see #getMetadata()
*/
private Metadata metadata;
/**
* Identifiers used by a resource. Identifiers must be unique in the data store,
* so after an identifier has been used it cannot be reused anymore even if the
* resource having that identifier has been removed.
* Values associated to identifiers tell whether the resource still exist.
*
* @see WorldFileResource#getIdentifier()
*/
final Map<String,Boolean> identifiers;
/**
* Creates a new store from the given file, URL or stream.
*
* @param provider the factory that created this {@code DataStore} instance, or {@code null} if unspecified.
* @param connector information about the storage (URL, stream, <i>etc</i>).
* @throws DataStoreException if an error occurred while opening the stream.
* @throws IOException if an error occurred while creating the image reader instance.
*/
public WorldFileStore(final WorldFileStoreProvider provider, final StorageConnector connector)
throws DataStoreException, IOException
{
this(new FormatFinder(provider, connector), true);
}
/**
* Creates a new store from the given file, URL or stream.
*
* @param format information about the storage (URL, stream, <i>etc</i>) and the reader/writer to use.
* @param readOnly {@code true} if the store should be open in read-only mode, ignoring {@code format}.
* This is a workaround while waiting for JEP 447: Statements before super(…).
* @throws DataStoreException if an error occurred while opening the stream.
* @throws IOException if an error occurred while creating the image reader instance.
*/
WorldFileStore(final FormatFinder format, final boolean readOnly) throws DataStoreException, IOException {
super(format.provider, format.connector);
listeners.useReadOnlyEvents();
identifiers = new HashMap<>();
suffix = format.suffix;
if (format.storage instanceof Closeable) {
toClose = (Closeable) format.storage;
}
if (readOnly || !format.openAsWriter) {
reader = format.getOrCreateReader();
if (reader == null) {
throw new UnsupportedStorageException(super.getLocale(), WorldFileStoreProvider.NAME,
format.storage, format.connector.getOption(OptionKey.OPEN_OPTIONS));
}
configureReader();
if (readOnly) {
format.close();
}
/*
* Do not invoke any method that may cause the image reader to start reading the stream,
* because the `WritableStore` subclass will want to save the initial stream position.
*/
}
}
/**
* Sets the locale to use for warning messages, if supported. If the reader
* does not support the locale, the reader's default locale will be used.
*/
private void configureReader() {
@SuppressWarnings("LocalVariableHidesMemberVariable")
final ImageReader reader = this.reader;
try {
reader.setLocale(listeners.getLocale());
} catch (IllegalArgumentException e) {
// Ignore
}
reader.addIIOReadWarningListener(new WarningListener(listeners));
}
/**
* Returns the preferred suffix for the auxiliary world file. For TIFF images, this is {@code "tfw"}.
* This method tries to use the same case (lower-case or upper-case) than the suffix of the main file.
*/
private String getWorldFileSuffix() {
if (suffix != null) {
final int length = suffix.length();
if (suffix.codePointCount(0, length) >= 2) {
boolean lower = true;
for (int i = length; i > 0;) {
final int c = suffix.codePointBefore(i);
lower = Character.isLowerCase(c); if ( lower) break;
lower = !Character.isUpperCase(c); if (!lower) break;
i -= Character.charCount(c);
}
// If the case cannot be determined, `lower` will default to `true`.
return new StringBuilder(3)
.appendCodePoint(suffix.codePointAt(0))
.appendCodePoint(suffix.codePointBefore(length))
.append(lower ? 'w' : 'W').toString();
}
}
return DEFAULT_SUFFIX;
}
/**
* Reads the "World file" by searching for an auxiliary file with a suffix inferred from
* the suffix of the main file. This method tries suffixes with the following conventions,
* in preference order.
*
* <ol>
* <li>First letter of main file suffix, followed by last letter, followed by {@code 'w'}.</li>
* <li>Full suffix of the main file followed by {@code 'w'}.</li>
* <li>{@value #DEFAULT_SUFFIX}.</li>
* </ol>
*
* @return the "World file" content as an affine transform, or {@code null} if none was found.
* @throws URISyntaxException if an error occurred while normalizing the URI.
* @throws IOException if an I/O error occurred.
* @throws DataStoreException if the auxiliary file content cannot be parsed.
*/
private AffineTransform2D readWorldFile() throws URISyntaxException, IOException, DataStoreException {
IOException warning = null;
final String preferred = getWorldFileSuffix();
loop: for (int convention=0;; convention++) {
final String wld;
switch (convention) {
default: break loop;
case 0: wld = preferred; break; // First file suffix to search.
case 2: wld = DEFAULT_SUFFIX; break; // File suffix to search in last resort.
case 1: {
if (preferred.equals(DEFAULT_SUFFIX)) break loop;
wld = suffix + preferred.charAt(preferred.length() - 1);
break;
}
}
try {
return readWorldFile(wld);
} catch (NoSuchFileException | FileNotFoundException e) {
if (warning == null) {
warning = e;
} else {
warning.addSuppressed(e);
}
}
}
if (warning != null) {
cannotReadAuxiliaryFile(WorldFileStore.class, "getGridGeometry", preferred, warning, true);
}
return null;
}
/**
* Reads the "World file" by parsing an auxiliary file with the given suffix.
*
* @param wld suffix of the auxiliary file.
* @return the "World file" content as an affine transform, or {@code null} if none was found.
* @throws URISyntaxException if an error occurred while normalizing the URI.
* @throws IOException if an I/O error occurred.
* @throws DataStoreException if the file content cannot be parsed.
*/
private AffineTransform2D readWorldFile(final String wld)
throws URISyntaxException, IOException, DataStoreException
{
final AuxiliaryContent content = readAuxiliaryFile(wld, false);
if (content == null) {
cannotReadAuxiliaryFile(WorldFileStore.class, "getGridGeometry", wld, null, true);
return null;
}
final String filename = content.getFilename();
final CharSequence[] lines = CharSequences.splitOnEOL(content);
final int expected = 6; // Expected number of elements.
int count = 0; // Actual number of elements.
final double[] elements = new double[expected];
for (int i=0; i<expected; i++) {
final String line = lines[i].toString().trim();
if (!line.isEmpty() && line.charAt(0) != '#') {
if (count >= expected) {
throw new DataStoreContentException(errors().getString(Errors.Keys.TooManyOccurrences_2, expected, "coefficient"));
}
try {
elements[count++] = Double.parseDouble(line);
} catch (NumberFormatException e) {
throw new DataStoreContentException(errors().getString(Errors.Keys.ErrorInFileAtLine_2, filename, i), e);
}
}
}
if (count != expected) {
throw new EOFException(errors().getString(Errors.Keys.UnexpectedEndOfFile_1, filename));
}
if (filename != null) {
final int s = filename.lastIndexOf(IOUtilities.EXTENSION_SEPARATOR);
if (s >= 0) {
suffixWLD = filename.substring(s+1);
}
}
return new AffineTransform2D(elements);
}
/**
* Returns the localized resources for producing error messages.
*/
private Errors errors() {
return Errors.forLocale(getLocale());
}
/**
* Returns the Image I/O format names or MIME types of the image read by this data store.
* More than one names may be returned if the format has aliases or if the MIME type
* has legacy types (e.g. official {@code "image/png"} and legacy {@code "image/x-png"}).
*
* @param asMimeType {@code true} for MIME types, or {@code false} for format names.
* @return the requested names, or an empty array if none or unknown.
*/
public String[] getImageFormat(final boolean asMimeType) {
@SuppressWarnings("LocalVariableHidesMemberVariable")
final ImageReader reader = this.reader;
if (reader != null) {
final ImageReaderSpi p = reader.getOriginatingProvider();
if (p != null) {
final String[] names = asMimeType ? p.getMIMETypes() : p.getFormatNames();
if (names != null) {
return names;
}
}
}
return CharSequences.EMPTY_ARRAY;
}
/**
* Returns paths to the main file together with auxiliary files.
*
* @return paths to the main file and auxiliary files, or an empty array if unknown.
* @throws DataStoreException if the URI cannot be converted to a {@link Path}.
*/
@Override
public synchronized Path[] getComponentFiles() throws DataStoreException {
if (suffixWLD == null) try {
getGridGeometry(MAIN_IMAGE); // Will compute `suffixWLD` as a side effect.
} catch (URISyntaxException | IOException e) {
throw new DataStoreException(e);
}
return listComponentFiles(suffixWLD, PRJ); // `suffixWLD` still null if file was not found.
}
/**
* Gets the grid geometry for image at the given index.
* This method should be invoked only once per image, and the result cached.
*
* @param index index of the image for which to read the grid geometry.
* @return grid geometry of the image at the given index.
* @throws IndexOutOfBoundsException if the image index is out of bounds.
* @throws URISyntaxException if an error occurred while normalizing the URI.
* @throws IOException if an I/O error occurred.
* @throws DataStoreException if the {@code *.prj} or {@code *.tfw} auxiliary file content cannot be parsed.
*/
final GridGeometry getGridGeometry(final int index) throws URISyntaxException, IOException, DataStoreException {
assert Thread.holdsLock(this);
@SuppressWarnings("LocalVariableHidesMemberVariable")
final ImageReader reader = reader();
if (gridGeometry == null) {
final AffineTransform2D gridToCRS;
width = reader.getWidth (MAIN_IMAGE);
height = reader.getHeight(MAIN_IMAGE);
gridToCRS = readWorldFile();
readPRJ(WorldFileStore.class, "getGridGeometry");
gridGeometry = new GridGeometry(new GridExtent(width, height), CELL_ANCHOR, gridToCRS, crs);
}
if (index != MAIN_IMAGE) {
final int w = reader.getWidth (index);
final int h = reader.getHeight(index);
if (w != width || h != height) {
// Cannot use `gridToCRS` and `crs` because they may not apply.
return new GridGeometry(new GridExtent(w, h), CELL_ANCHOR, null, null);
}
}
return gridGeometry;
}
/**
* Sets the store-wide grid geometry when a new coverage is written. The {@link WritableStore} implementation
* is responsible for making sure that the new grid geometry is compatible with preexisting grid geometry.
*
* @param index index of the image for which to set the grid geometry.
* @param gg the new grid geometry.
* @return suffix of the "world file", or {@code null} if the image cannot be written.
*/
String setGridGeometry(final int index, final GridGeometry gg)
throws URISyntaxException, IOException, DataStoreException
{
if (index != MAIN_IMAGE) {
return null;
}
final GridExtent extent = gg.getExtent();
final int w = Math.toIntExact(extent.getSize(WorldFileResource.X_DIMENSION));
final int h = Math.toIntExact(extent.getSize(WorldFileResource.Y_DIMENSION));
final String s = (suffixWLD != null) ? suffixWLD : getWorldFileSuffix();
crs = gg.isDefined(GridGeometry.CRS) ? gg.getCoordinateReferenceSystem() : null;
gridGeometry = gg; // Set only after success of all the above.
width = w;
height = h;
suffixWLD = s;
return s;
}
/**
* Returns information about the data store as a whole.
*/
@Override
public synchronized Metadata getMetadata() throws DataStoreException {
if (metadata == null) try {
final MetadataBuilder builder = new MetadataBuilder();
String format = reader().getFormatName();
for (final String key : KNOWN_FORMATS) {
if (key.equalsIgnoreCase(format)) {
try {
builder.setPredefinedFormat(key);
format = null;
} catch (MetadataStoreException e) {
listeners.warning(Level.FINE, null, e);
}
break;
}
}
builder.addFormatName(format); // Does nothing if `format` is null.
builder.addResourceScope(ScopeCode.COVERAGE, null);
builder.addSpatialRepresentation(null, getGridGeometry(MAIN_IMAGE), true);
if (gridGeometry.isDefined(GridGeometry.ENVELOPE)) {
builder.addExtent(gridGeometry.getEnvelope(), listeners);
}
mergeAuxiliaryMetadata(WorldFileStore.class, builder);
builder.addTitleOrIdentifier(getFilename(), MetadataBuilder.Scope.ALL);
builder.setISOStandards(false);
metadata = builder.buildAndFreeze();
} catch (URISyntaxException | IOException e) {
throw new DataStoreException(e);
}
return metadata;
}
/**
* Returns all images in this store. Note that fetching the size of the list is a potentially costly operation.
*
* @return list of images in this store.
* @throws DataStoreException if an error occurred while fetching components.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public synchronized Collection<? extends GridCoverageResource> components() throws DataStoreException {
if (components == null) try {
components = new Components(reader().getNumImages(false));
} catch (IOException e) {
throw new DataStoreException(e);
}
return components;
}
/**
* Returns all images in this store, or {@code null} if none and {@code create} is false.
*
* @param create whether to create the component list if it was not already created.
* @param numImages number of images, or any negative value if unknown.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
final Components components(final boolean create, final int numImages) {
if (components == null && create) {
components = new Components(numImages);
}
return components;
}
/**
* A list of images where each {@link WorldFileResource} instance is initialized when first needed.
* Fetching the list size may be a costly operation and will be done only if requested.
*/
final class Components extends ListOfUnknownSize<WorldFileResource> {
/**
* Size of this list, or any negative value if unknown.
*/
private int size;
/**
* All elements in this list. Some array elements may be {@code null} if the image
* has never been requested.
*/
private WorldFileResource[] images;
/**
* Creates a new list of images.
*
* @param numImages number of images, or any negative value if unknown.
*/
private Components(final int numImages) {
size = numImages;
images = new WorldFileResource[Math.max(numImages, 1)];
}
/**
* Returns the number of images in this list.
* This method may be costly when invoked for the first time.
*/
@Override
public int size() {
synchronized (WorldFileStore.this) {
if (size < 0) try {
size = reader().getNumImages(true);
images = ArraysExt.resize(images, size);
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (DataStoreException e) {
throw new BackingStoreException(e);
}
return size;
}
}
/**
* Returns the number of images if this information is known, or any negative value otherwise.
* This is used by {@link ListOfUnknownSize} for optimizing some operations.
*/
@Override
protected int sizeIfKnown() {
synchronized (WorldFileStore.this) {
return size;
}
}
/**
* Returns {@code true} if an element exists at the given index.
* Current implementations is not more efficient than {@link #get(int)}.
*/
@Override
protected boolean exists(final int index) {
synchronized (WorldFileStore.this) {
if (size >= 0) {
return index >= 0 && index < size;
}
try {
return get(index) != null;
} catch (IndexOutOfBoundsException e) {
return false;
}
}
}
/**
* Returns the image at the given index. New instances are created when first requested.
*
* @param index index of the image for which to get a resource.
* @return resource for the image identified by the given index.
* @throws IndexOutOfBoundsException if the image index is out of bounds.
*/
@Override
public WorldFileResource get(final int index) {
synchronized (WorldFileStore.this) {
WorldFileResource image = null;
if (index < images.length) {
image = images[index];
}
if (image == null) try {
image = createImageResource(index);
if (index >= images.length) {
images = Arrays.copyOf(images, Math.max(images.length * 2, index + 1));
}
images[index] = image;
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (URISyntaxException | DataStoreException e) {
throw new BackingStoreException(e);
}
return image;
}
}
/**
* Invoked <em>after</em> an image has been added to the image file.
* This method adds in this list a reference to the newly added file.
*
* @param image the image to add to this list.
*/
final void added(final WorldFileResource image) {
size = image.getImageIndex();
if (size >= images.length) {
images = Arrays.copyOf(images, size * 2);
}
images[size++] = image;
}
/**
* Invoked <em>after</em> an image has been removed from the image file.
* This method performs no bounds check (it must be done by the caller).
*
* @param index index of the image that has been removed.
*/
final void removed(int index) throws DataStoreException {
final int last = images.length - 1;
System.arraycopy(images, index+1, images, index, last - index);
images[last] = null;
size--;
while (index < last) {
final WorldFileResource image = images[index++];
if (image != null) image.decrementImageIndex();
}
}
/**
* Removes the element at the specified position in this list.
*/
@Override
public WorldFileResource remove(final int index) {
final WorldFileResource image = get(index);
try {
WorldFileStore.this.remove(image);
} catch (DataStoreException e) {
throw new UnsupportedOperationException(e);
}
return image;
}
}
/**
* Invoked by {@link Components} when the caller want to remove a resource.
* The actual implementation is provided by {@link WritableStore}.
*/
void remove(final Resource resource) throws DataStoreException {
throw new ReadOnlyStorageException();
}
/**
* Creates a {@link GridCoverageResource} for the specified image.
* This method is invoked by {@link Components} when first needed
* and the result is cached by the caller.
*
* @param index index of the image for which to create a resource.
* @return resource for the image identified by the given index.
* @throws IndexOutOfBoundsException if the image index is out of bounds.
* @throws URISyntaxException if an error occurred while normalizing the URI.
*/
WorldFileResource createImageResource(final int index)
throws DataStoreException, URISyntaxException, IOException
{
return new WorldFileResource(this, listeners, index, getGridGeometry(index));
}
/**
* Whether the component of this data store is used only as a delegate.
* This is {@code false} when the components will be given to the user,
* or {@code true} if the singleton component will be used only for internal purposes.
*/
boolean isComponentHidden() {
return false;
}
/**
* Prepares an image reader compatible with the writer and sets its input.
* This method is invoked for switching from write mode to read mode.
* Its actual implementation is provided by {@link WritableResource}.
*
* @param current the current image reader, or {@code null} if none.
* @return the image reader to use, or {@code null} if none.
* @throws IOException if an error occurred while preparing the reader.
*/
ImageReader prepareReader(ImageReader current) throws IOException {
return null;
}
/**
* Returns the reader without doing any validation. The reader may be {@code null} either
* because the store is closed or because the store is initially opened in write-only mode.
* The reader may have a {@code null} input.
*/
final ImageReader getCurrentReader() {
return reader;
}
/**
* Returns the reader if it has not been closed.
*
* @throws DataStoreClosedException if this data store is closed.
* @throws IOException if an error occurred while preparing the reader.
*/
final ImageReader reader() throws DataStoreException, IOException {
assert Thread.holdsLock(this);
ImageReader current = reader;
if (current == null || current.getInput() == null) {
reader = current = prepareReader(current);
if (current == null) {
throw new DataStoreClosedException(getLocale(), WorldFileStoreProvider.NAME, StandardOpenOption.READ);
}
configureReader();
}
return current;
}
/**
* Closes this data store and releases any underlying resources.
* If a read operation is in progress, it will be aborted.
*
* @throws DataStoreException if an error occurred while closing this data store.
*/
@Override
public void close() throws DataStoreException {
listeners.close(); // Should never fail.
final ImageReader codec = reader;
if (codec != null) codec.abort();
synchronized (this) {
final Closeable stream = toClose;
reader = null;
toClose = null;
metadata = null;
components = null;
gridGeometry = null;
try {
Object input = null;
if (codec != null) {
input = codec.getInput();
codec.reset();
codec.dispose();
if (input instanceof AutoCloseable) {
((AutoCloseable) input).close();
}
}
if (stream != null && stream != input) {
stream.close();
}
} catch (Exception e) {
throw new DataStoreException(e);
}
}
}
}