| /* |
| * 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.geotiff; |
| |
| import java.util.Locale; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.logging.LogRecord; |
| import java.net.URI; |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.StandardOpenOption; |
| import org.opengis.util.NameSpace; |
| import org.opengis.util.NameFactory; |
| import org.opengis.util.GenericName; |
| import org.opengis.util.FactoryException; |
| import org.opengis.metadata.Metadata; |
| import org.opengis.metadata.maintenance.ScopeCode; |
| import org.opengis.parameter.ParameterValueGroup; |
| import org.apache.sis.setup.OptionKey; |
| import org.apache.sis.storage.Aggregate; |
| import org.apache.sis.storage.GridCoverageResource; |
| import org.apache.sis.storage.DataStore; |
| import org.apache.sis.storage.StorageConnector; |
| import org.apache.sis.storage.DataStoreException; |
| import org.apache.sis.storage.DataStoreContentException; |
| import org.apache.sis.storage.UnsupportedStorageException; |
| import org.apache.sis.storage.DataStoreClosedException; |
| import org.apache.sis.storage.IllegalNameException; |
| import org.apache.sis.storage.event.StoreEvent; |
| import org.apache.sis.storage.event.StoreListener; |
| import org.apache.sis.storage.event.StoreListeners; |
| import org.apache.sis.storage.event.WarningEvent; |
| import org.apache.sis.internal.storage.io.ChannelDataInput; |
| import org.apache.sis.internal.storage.io.IOUtilities; |
| import org.apache.sis.internal.storage.MetadataBuilder; |
| import org.apache.sis.internal.storage.StoreUtilities; |
| import org.apache.sis.internal.storage.URIDataStore; |
| import org.apache.sis.internal.util.Constants; |
| import org.apache.sis.internal.util.Numerics; |
| import org.apache.sis.internal.util.ListOfUnknownSize; |
| import org.apache.sis.metadata.sql.MetadataStoreException; |
| import org.apache.sis.util.collection.BackingStoreException; |
| import org.apache.sis.util.resources.Errors; |
| |
| |
| /** |
| * A data store backed by GeoTIFF files. |
| * |
| * @author Rémi Maréchal (Geomatys) |
| * @author Martin Desruisseaux (Geomatys) |
| * @author Thi Phuong Hao Nguyen (VNSC) |
| * @version 1.0 |
| * @since 0.8 |
| * @module |
| */ |
| public class GeoTiffStore extends DataStore implements Aggregate { |
| /** |
| * The encoding of strings in the metadata. The string specification said that is shall be US-ASCII, |
| * but Apache SIS nevertheless let the user specifies an alternative encoding if needed. |
| */ |
| final Charset encoding; |
| |
| /** |
| * The GeoTIFF reader implementation, or {@code null} if the store has been closed. |
| */ |
| private Reader reader; |
| |
| /** |
| * The {@link GeoTiffStoreProvider#LOCATION} parameter value, or {@code null} if none. |
| * This is used for information purpose only, not for actual reading operations. |
| * |
| * @see #getOpenParameters() |
| */ |
| private final URI location; |
| |
| /** |
| * The data store identifier created from the filename, or {@code null} if none. |
| * Defined as a namespace for use as the scope of children resources (the images). |
| */ |
| final NameSpace identifier; |
| |
| /** |
| * The metadata, or {@code null} if not yet created. |
| * |
| * @see #getMetadata() |
| */ |
| private Metadata metadata; |
| |
| /** |
| * Description of images in this GeoTIFF files. This collection is created only when first needed. |
| * |
| * @see #components() |
| */ |
| private List<GridCoverageResource> components; |
| |
| /** |
| * Creates a new GeoTIFF store from the given file, URL or stream object. |
| * This constructor invokes {@link StorageConnector#closeAllExcept(Object)}, |
| * keeping open only the needed resource. |
| * |
| * @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 GeoTIFF file. |
| */ |
| public GeoTiffStore(final GeoTiffStoreProvider provider, final StorageConnector connector) throws DataStoreException { |
| super(provider, connector); |
| final Charset encoding = connector.getOption(OptionKey.ENCODING); |
| this.encoding = (encoding != null) ? encoding : StandardCharsets.US_ASCII; |
| final ChannelDataInput input = connector.getStorageAs(ChannelDataInput.class); |
| if (input == null) { |
| throw new UnsupportedStorageException(super.getLocale(), Constants.GEOTIFF, |
| connector.getStorage(), connector.getOption(OptionKey.OPEN_OPTIONS)); |
| } |
| location = connector.getStorageAs(URI.class); |
| connector.closeAllExcept(input); |
| try { |
| reader = new Reader(this, input); |
| } catch (IOException e) { |
| throw new DataStoreException(e); |
| } |
| if (location != null) { |
| final NameFactory f = reader.nameFactory; |
| String filename = IOUtilities.filenameWithoutExtension(input.filename); |
| if (Numerics.isUnsignedInteger(filename)) filename += ".tiff"; |
| identifier = f.createNameSpace(f.createLocalName(null, filename), null); |
| } else { |
| // Location not convertible to URI. The string representation is probably a class name, which is not useful. |
| identifier = null; |
| } |
| } |
| |
| /** |
| * Opens access to listeners for {@link ImageFileDirectory}. |
| */ |
| final StoreListeners listeners() { |
| return listeners; |
| } |
| |
| /** |
| * Returns the parameters used to open this GeoTIFF data store. |
| * The parameters are described by {@link GeoTiffStoreProvider#getOpenParameters()} and contains at least |
| * a parameter named {@value org.apache.sis.storage.DataStoreProvider#LOCATION} with a {@link URI} value. |
| * The return value may be empty if the storage input can not be described by a URI |
| * (for example a GeoTIFF file reading directly from a {@link java.nio.channels.ReadableByteChannel}). |
| * |
| * @return parameters used for opening this data store. |
| */ |
| @Override |
| public Optional<ParameterValueGroup> getOpenParameters() { |
| return Optional.ofNullable(URIDataStore.parameters(provider, location)); |
| } |
| |
| /** |
| * Returns an identifier constructed from the name of the TIFF file. |
| * An identifier is available only if the storage input specified at construction time was something convertible to |
| * {@link java.net.URI}, for example an {@link java.net.URL}, {@link java.io.File} or {@link java.nio.file.Path}. |
| * |
| * @return the identifier derived from the filename. |
| * @throws DataStoreException if an error occurred while fetching the identifier. |
| * |
| * @since 1.0 |
| */ |
| @Override |
| public Optional<GenericName> getIdentifier() throws DataStoreException { |
| return (identifier != null) ? Optional.of(identifier.name()) : Optional.empty(); |
| } |
| |
| /** |
| * Returns information about the dataset as a whole. The returned metadata object can contain information |
| * such as the spatiotemporal extent of the dataset, contact information about the creator or distributor, |
| * data quality, usage constraints and more. |
| * |
| * @return information about the dataset. |
| * @throws DataStoreException if an error occurred while reading the data. |
| */ |
| @Override |
| public synchronized Metadata getMetadata() throws DataStoreException { |
| if (metadata == null) { |
| final Reader reader = reader(); |
| final MetadataBuilder builder = reader.metadata; |
| try { |
| builder.setFormat(Constants.GEOTIFF); |
| } catch (MetadataStoreException e) { |
| builder.addFormatName(Constants.GEOTIFF); |
| listeners.warning(e); |
| } |
| builder.addEncoding(encoding, MetadataBuilder.Scope.METADATA); |
| builder.addResourceScope(ScopeCode.COVERAGE, null); |
| final Locale locale = getLocale(); |
| int n = 0; |
| try { |
| ImageFileDirectory dir; |
| while ((dir = reader.getImageFileDirectory(n++)) != null) { |
| dir.completeMetadata(builder, locale); |
| } |
| } catch (IOException e) { |
| throw errorIO(e); |
| } catch (FactoryException | ArithmeticException e) { |
| throw new DataStoreContentException(getLocale(), Constants.GEOTIFF, reader.input.filename, null).initCause(e); |
| } |
| /* |
| * Add the filename as an identifier only if the input was something convertible to URI (URL, File or Path), |
| * otherwise reader.input.filename may not be useful; it may be just the InputStream classname. If the TIFF |
| * file did not specified any ImageDescription tag, then we will had the filename as a title instead than an |
| * identifier because the title is mandatory in ISO 19115 metadata. |
| */ |
| getIdentifier().ifPresent((id) -> builder.addTitleOrIdentifier(id.toString(), MetadataBuilder.Scope.ALL)); |
| builder.setISOStandards(true); |
| metadata = builder.build(true); |
| } |
| return metadata; |
| } |
| |
| /** |
| * Returns the exception to throw when an I/O error occurred. |
| */ |
| private DataStoreException errorIO(final IOException e) { |
| return new DataStoreException(errors().getString(Errors.Keys.CanNotRead_1, reader.input.filename), e); |
| } |
| |
| /** |
| * Returns the reader if it is not closed, or thrown an exception otherwise. |
| */ |
| private Reader reader() throws DataStoreException { |
| final Reader r = reader; |
| if (r == null) { |
| throw new DataStoreClosedException(getLocale(), Constants.GEOTIFF, StandardOpenOption.READ); |
| } |
| return r; |
| } |
| |
| /** |
| * Returns descriptions of all images in this GeoTIFF file. |
| * Images are not immediately loaded. |
| * |
| * <p>If an error occurs during iteration in the returned collection, |
| * an unchecked {@link BackingStoreException} will be thrown with a {@link DataStoreException} as its cause.</p> |
| * |
| * @return descriptions of all images in this GeoTIFF file. |
| * @throws DataStoreException if an error occurred while fetching the image descriptions. |
| * |
| * @since 1.0 |
| */ |
| @Override |
| @SuppressWarnings("ReturnOfCollectionOrArrayField") |
| public List<GridCoverageResource> components() throws DataStoreException { |
| if (components == null) { |
| components = new Components(); |
| } |
| return components; |
| } |
| |
| /** |
| * The components returned by {@link #components}. Defined as a named class instead than an anonymous |
| * class for more readable stack trace. This is especially useful since {@link BackingStoreException} |
| * may happen in any method. |
| */ |
| private final class Components extends ListOfUnknownSize<GridCoverageResource> { |
| /** The collection size, cached when first computed. */ |
| private int size = -1; |
| |
| /** Returns the size or -1 if not yet known. */ |
| @Override protected int sizeIfKnown() { |
| return size; |
| } |
| |
| /** Returns the size, computing and caching it if needed. */ |
| @Override public int size() { |
| if (size < 0) { |
| size = super.size(); |
| } |
| return size; |
| } |
| |
| /** Returns whether the given index is valid. */ |
| @Override protected boolean exists(final int index) { |
| return (index >= 0) && getImageFileDirectory(index) != null; |
| } |
| |
| /** Returns element at the given index or throw {@link IndexOutOfBoundsException}. */ |
| @Override public GridCoverageResource get(final int index) { |
| if (index >= 0) { |
| GridCoverageResource image = getImageFileDirectory(index); |
| if (image != null) return image; |
| } |
| throw new IndexOutOfBoundsException(errors().getString(Errors.Keys.IndexOutOfBounds_1, index)); |
| } |
| |
| /** Returns element at the given index or returns {@code null} if the index is invalid. */ |
| private GridCoverageResource getImageFileDirectory(final int index) { |
| try { |
| return reader().getImageFileDirectory(index); |
| } catch (IOException e) { |
| throw new BackingStoreException(errorIO(e)); |
| } catch (DataStoreException e) { |
| throw new BackingStoreException(e); |
| } |
| } |
| } |
| |
| /** |
| * Returns the image at the given index. Images numbering starts at 1. |
| * |
| * @param sequence string representation of the image index, starting at 1. |
| * @return image at the given index. |
| * @throws DataStoreException if the requested image can not be obtained. |
| */ |
| @Override |
| public GridCoverageResource findResource(final String sequence) throws DataStoreException { |
| Exception cause; |
| int index; |
| try { |
| index = Integer.parseInt(sequence); |
| cause = null; |
| } catch (NumberFormatException e) { |
| index = 0; |
| cause = e; |
| } |
| if (index > 0) try { |
| ImageFileDirectory image = reader().getImageFileDirectory(index - 1); |
| if (image != null) return image; |
| } catch (IOException e) { |
| throw errorIO(e); |
| } |
| throw new IllegalNameException(StoreUtilities.resourceNotFound(this, sequence), cause); |
| } |
| |
| /** |
| * Registers a listener to notify when the specified kind of event occurs in this data store. |
| * The current implementation of this data store can emit only {@link WarningEvent}s; |
| * any listener specified for another kind of events will be ignored. |
| */ |
| @Override |
| public <T extends StoreEvent> void addListener(Class<T> eventType, StoreListener<? super T> listener) { |
| // If an argument is null, we let the parent class throws (indirectly) NullArgumentException. |
| if (listener == null || eventType == null || eventType.isAssignableFrom(WarningEvent.class)) { |
| super.addListener(eventType, listener); |
| } |
| } |
| |
| /** |
| * Closes this GeoTIFF store and releases any underlying resources. |
| * |
| * @throws DataStoreException if an error occurred while closing the GeoTIFF file. |
| */ |
| @Override |
| public synchronized void close() throws DataStoreException { |
| final Reader r = reader; |
| reader = null; |
| if (r != null) try { |
| r.close(); |
| } catch (IOException e) { |
| throw new DataStoreException(e); |
| } |
| } |
| |
| /** |
| * Returns the error resources in the current locale. |
| */ |
| final Errors errors() { |
| return Errors.getResources(getLocale()); |
| } |
| |
| /** |
| * Reports a warning represented by the given message and exception. |
| * At least one of {@code message} and {@code exception} shall be non-null. |
| * |
| * @param message the message to log, or {@code null} if none. |
| * @param exception the exception to log, or {@code null} if none. |
| */ |
| final void warning(final String message, final Exception exception) { |
| listeners.warning(message, exception); |
| } |
| |
| /** |
| * Reports a warning contained in the given {@link LogRecord}. |
| * Note that the given record will not necessarily be sent to the logging framework; |
| * if the user as registered at least one listener, then the record will be sent to the listeners instead. |
| * |
| * <p>This method sets the {@linkplain LogRecord#setSourceClassName(String) source class name} and |
| * {@linkplain LogRecord#setSourceMethodName(String) source method name} to hard-coded values. |
| * Those values assume that the warnings occurred indirectly from a call to {@link #getMetadata()} |
| * in this class. We do not report private classes or methods as the source of warnings.</p> |
| * |
| * @param record the warning to report. |
| */ |
| final void warning(final LogRecord record) { |
| // Logger name will be set by listeners.warning(record). |
| record.setSourceClassName(GeoTiffStore.class.getName()); |
| record.setSourceMethodName("getMetadata"); |
| listeners.warning(record); |
| } |
| } |