This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 9e5697263de0940bb3567249f0fa7424d1215f71 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed May 4 18:59:15 2022 +0200 If the World File uses a format which is known to support only one image (PNG, BMP, WBMP and maybe JPEG), returns a data store which implements directly `GridCoverageResource` instead of `Aggregate`. --- .../sis/internal/storage/image/FormatFilter.java | 19 +- .../sis/internal/storage/image/FormatFinder.java | 288 +++++++++++++++++++++ .../internal/storage/image/MultiImageStore.java | 62 +++++ .../internal/storage/image/SingleImageStore.java | 191 ++++++++++++++ .../internal/storage/image/WorldFileResource.java | 5 +- .../sis/internal/storage/image/WorldFileStore.java | 116 +++------ .../storage/image/WorldFileStoreProvider.java | 67 +++-- .../sis/internal/storage/image/WritableStore.java | 81 ++---- .../storage/image/SelfConsistencyTest.java | 2 +- .../internal/storage/image/WorldFileStoreTest.java | 12 +- 10 files changed, 681 insertions(+), 162 deletions(-) diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java index ceda08a109..0d1056b864 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java @@ -168,14 +168,15 @@ enum FormatFilter { * This is intentionally not done automatically by {@link StorageConnector}. * * @param identifier the property value to use as a filtering criterion, or {@code null} if none. - * @param connector provider of the input to be given to the new reader instance. + * @param format provider of the input to be given to the new reader instance. * @param deferred initially empty map to be populated with providers tested by this method. * @return the new image reader instance with its input initialized, or {@code null} if none was found. * @throws DataStoreException if an error occurred while opening a stream from the storage connector. * @throws IOException if an error occurred while creating the image reader instance. */ - final ImageReader createReader(final String identifier, final StorageConnector connector, - final Map<ImageReaderSpi,Boolean> deferred) throws IOException, DataStoreException + final ImageReader createReader(final String identifier, final FormatFinder format, + final Map<ImageReaderSpi,Boolean> deferred) + throws IOException, DataStoreException { final Iterator<ImageReaderSpi> it = getServiceProviders(ImageReaderSpi.class, identifier); while (it.hasNext()) { @@ -183,12 +184,12 @@ enum FormatFilter { if (deferred.putIfAbsent(provider, Boolean.FALSE) == null) { for (final Class<?> type : provider.getInputTypes()) { if (ArraysExt.contains(VALID_INPUTS, type)) { - final Object input = connector.getStorageAs(type); + final Object input = format.connector.getStorageAs(type); if (input != null) { if (provider.canDecodeInput(input)) { - connector.closeAllExcept(input); final ImageReader reader = provider.createReaderInstance(); reader.setInput(input, false, true); + format.keepOpen = input; return reader; } break; // Skip other input types, try the next provider. @@ -209,14 +210,14 @@ enum FormatFilter { * This is intentionally not done automatically by {@link StorageConnector}. * * @param identifier the property value to use as a filtering criterion, or {@code null} if none. - * @param output the output to be given to the new reader instance. + * @param format provider of the output to be given to the new writer instance. * @param image the image to write, or {@code null} if unknown. * @param deferred initially empty map to be populated with providers tested by this method. * @return the new image writer instance with its output initialized, or {@code null} if none was found. * @throws DataStoreException if an error occurred while opening a stream from the storage connector. * @throws IOException if an error occurred while creating the image writer instance. */ - final ImageWriter createWriter(final String identifier, final StorageConnector connector, final RenderedImage image, + final ImageWriter createWriter(final String identifier, final FormatFinder format, final RenderedImage image, final Map<ImageWriterSpi,Boolean> deferred) throws IOException, DataStoreException { final Iterator<ImageWriterSpi> it = getServiceProviders(ImageWriterSpi.class, identifier); @@ -226,11 +227,11 @@ enum FormatFilter { if (image == null || provider.canEncodeImage(image)) { for (final Class<?> type : provider.getOutputTypes()) { if (ArraysExt.contains(VALID_OUTPUTS, type)) { - final Object output = connector.getStorageAs(type); + final Object output = format.connector.getStorageAs(type); if (output != null) { - connector.closeAllExcept(output); final ImageWriter writer = provider.createWriterInstance(); writer.setOutput(output); + format.keepOpen = output; return writer; } } diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFinder.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFinder.java new file mode 100644 index 0000000000..d4f7f83837 --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFinder.java @@ -0,0 +1,288 @@ +/* + * 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.internal.storage.image; + +import java.util.Map; +import java.util.LinkedHashMap; +import java.io.DataOutput; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Files; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriter; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.FileImageOutputStream; +import org.apache.sis.storage.StorageConnector; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.internal.storage.io.IOUtilities; + + +/** + * Helper class for finding the {@link ImageReader} or {@link ImageWriter} instance to use. + * This is a temporary object used only at {@link WorldFileStore} construction time. + * It also helps to choose which {@link WorldFileStore} subclass to instantiate. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +final class FormatFinder implements AutoCloseable { + /** + * The factory that created this {@code DataStore} instance, or {@code null} if unspecified. + */ + final WorldFileStoreProvider provider; + + /** + * Information about the storage (URL, stream, <i>etc</i>). + */ + final StorageConnector connector; + + /** + * The {@link #connector} object to keep open if we successfully created a {@link WorldFileStore}. + * This is often the same object than {@link #storage} but may be different if an {@link ImageInputStream} + * has been created from the storage object. + * + * <p>This value is {@code null} until successful instantiation of image reader or writer. + * A null value means to close everything, which is the desired behavior in case of failure.</p> + */ + Object keepOpen; + + /** + * The file, URL or stream where to read or write the image. + * If the {@linkplain StorageConnector#getStorage() user-specified storage} was an {@link ImageReader} + * or {@link ImageWriter}, then the value stored in this field is the input/output of the reader/writer. + */ + final Object storage; + + /** + * The image reader if specified or created by this {@code FormatFinder}, or {@code null}. + */ + private ImageReader reader; + + /** + * The image writer if specified or created by this {@code FormatFinder}, or {@code null}. + */ + private ImageWriter writer; + + /** + * Whether we already made an attempt to find the image reader or writer using {@link ImageIO} registry. + */ + private boolean readerLookupDone, writerLookupDone; + + /** + * {@code true} if the {@linkplain #storage} seems to be writable. + */ + final boolean isWritable; + + /** + * {@code true} if the storage should be open is write mode instead of read mode. + * This is {@code true} if the file does not exist or the file is empty. + */ + final boolean openAsWriter; + + /** + * {@code true} if the file is known to be empty, or {@code false} in case of doubt. + */ + final boolean fileIsEmpty; + + /** + * The filename extension (may be an empty string), or {@code null} if unknown. + * It does not include the leading dot. + */ + final String suffix; + + /** + * Creates a new format finder. + * + * @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>). + */ + FormatFinder(final WorldFileStoreProvider provider, final StorageConnector connector) + throws DataStoreException, IOException + { + this.provider = provider; + this.connector = connector; + Object storage = connector.getStorage(); + if (storage instanceof ImageReader) { + reader = (ImageReader) storage; + storage = reader.getInput(); + readerLookupDone = true; + } else if (storage instanceof ImageWriter) { + writer = (ImageWriter) storage; + storage = writer.getOutput(); + writerLookupDone = true; + } + this.storage = storage; + this.suffix = IOUtilities.extension(storage); + /* + * Detect if the image can be opened in read/write mode. + * If not, it will be opened in read-only mode. + */ + if (writer != null) { + isWritable = true; + openAsWriter = true; + fileIsEmpty = false; + } else if (reader != null) { + isWritable = (reader.getInput() instanceof DataOutput); // Parent of ImageOutputStream. + openAsWriter = false; + fileIsEmpty = false; + } else { + isWritable = WorldFileStoreProvider.isWritable(connector); + if (isWritable) { + final Path path = connector.getStorageAs(Path.class); + if (path != null) { + fileIsEmpty = !Files.exists(path) || Files.size(path) == 0; + openAsWriter = fileIsEmpty; + return; + } + } + openAsWriter = false; + fileIsEmpty = false; + } + } + + /** + * Returns the name of the format. + * + * @return name of the format, or {@code null} if unknown. + */ + final String[] getFormatName() throws DataStoreException, IOException { + if (openAsWriter) { + final ImageWriter writer = getOrCreateWriter(); + if (writer != null) { + final ImageWriterSpi spi = writer.getOriginatingProvider(); + if (spi != null) { + return spi.getFormatNames(); + } + } + } else { + final ImageReader reader = getOrCreateReader(); + if (reader != null) { + final ImageReaderSpi spi = reader.getOriginatingProvider(); + if (spi != null) { + return spi.getFormatNames(); + } + } + } + return null; + } + + /** + * Returns the user-specified reader or searches for a reader that claim to be able to read the storage input. + * This method tries first the readers associated to the file suffix. If no reader is found, then this method + * tries all other readers. + * + * @return the reader, or {@code null} if none could be found. + */ + final ImageReader getOrCreateReader() throws DataStoreException, IOException { + if (!readerLookupDone) { + readerLookupDone = true; + final Map<ImageReaderSpi,Boolean> deferred = new LinkedHashMap<>(); + if (suffix != null) { + reader = FormatFilter.SUFFIX.createReader(suffix, this, deferred); + } + if (reader == null) { + reader = FormatFilter.SUFFIX.createReader(null, this, deferred); + if (reader == null) { + /* + * If no reader has been found, maybe `StorageConnector` has not been able to create + * an `ImageInputStream`. It may happen if the storage object is of unknown type. + * Check if it is the case, then try all providers that we couldn't try because of that. + */ + ImageInputStream stream = null; + for (final Map.Entry<ImageReaderSpi,Boolean> entry : deferred.entrySet()) { + if (entry.getValue()) { + if (stream == null) { + if (isWritable) { + // ImageOutputStream is both read and write. + stream = ImageIO.createImageOutputStream(storage); + } + if (stream == null) { + stream = ImageIO.createImageInputStream(storage); + if (stream == null) break; + } + } + final ImageReaderSpi p = entry.getKey(); + if (p.canDecodeInput(stream)) { + reader = p.createReaderInstance(); + reader.setInput(stream); + keepOpen = storage; + break; + } + } + } + } + } + } + return reader; + } + + /** + * Returns the user-specified writer or searches for a writer for the file suffix. + * + * @return the writer, or {@code null} if none could be found. + */ + final ImageWriter getOrCreateWriter() throws DataStoreException, IOException { + if (!writerLookupDone) { + writerLookupDone = true; + final Map<ImageWriterSpi,Boolean> deferred = new LinkedHashMap<>(); + if (suffix != null) { + writer = FormatFilter.SUFFIX.createWriter(suffix, this, null, deferred); + } + if (writer == null) { + writer = FormatFilter.SUFFIX.createWriter(null, this, null, deferred); + if (writer == null) { + ImageOutputStream stream = null; + for (final Map.Entry<ImageWriterSpi,Boolean> entry : deferred.entrySet()) { + if (entry.getValue()) { + if (stream == null) { + final File file = connector.getStorageAs(File.class); + if (file != null) { + stream = new FileImageOutputStream(file); + } else { + stream = ImageIO.createImageOutputStream(storage); + if (stream == null) break; + } + } + final ImageWriterSpi p = entry.getKey(); + writer = p.createWriterInstance(); + writer.setOutput(stream); + keepOpen = storage; + break; + } + } + } + } + } + return writer; + } + + /** + * Closes all unused resources. Keep open only the find of objects needed by the image reader or writer. + * This method must be invoked after by {@link WorldFileStore} construction. + */ + @Override + public final void close() throws DataStoreException { + connector.closeAllExcept(keepOpen); + } +} diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/MultiImageStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/MultiImageStore.java new file mode 100644 index 0000000000..5609c1ebd6 --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/MultiImageStore.java @@ -0,0 +1,62 @@ +/* + * 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.internal.storage.image; + +import java.io.IOException; +import org.apache.sis.storage.Aggregate; +import org.apache.sis.storage.WritableAggregate; +import org.apache.sis.storage.DataStoreException; + + +/** + * A world file store exposing in the public API the fact that it is an aggregate. + * This class is used for image formats that may store many images per file. + * Examples: TIFF and GIF image formats. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +final class MultiImageStore extends WorldFileStore implements Aggregate { + /** + * 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. + * @throws DataStoreException if an error occurred while opening the stream. + * @throws IOException if an error occurred while creating the image reader instance. + */ + MultiImageStore(final FormatFinder format) throws DataStoreException, IOException { + super(format, false); + } + + /** + * The writable variant of {@link MultiImageStore}. + */ + static final class Writable extends WritableStore implements WritableAggregate { + /** + * 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. + * @throws DataStoreException if an error occurred while opening the stream. + * @throws IOException if an error occurred while creating the image reader instance. + */ + Writable(final FormatFinder format) throws DataStoreException, IOException { + super(format); + } + } +} diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/SingleImageStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/SingleImageStore.java new file mode 100644 index 0000000000..1c540ca55e --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/SingleImageStore.java @@ -0,0 +1,191 @@ +/* + * 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.internal.storage.image; + +import java.util.List; +import java.util.Optional; +import java.io.IOException; +import org.opengis.geometry.Envelope; +import org.apache.sis.coverage.SampleDimension; +import org.apache.sis.coverage.grid.GridCoverage; +import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.GridCoverageResource; +import org.apache.sis.storage.RasterLoadingStrategy; +import org.apache.sis.storage.UnsupportedQueryException; +import org.apache.sis.storage.WritableGridCoverageResource; +import org.apache.sis.storage.Query; + + +/** + * A world file store which is expected to contain exactly one image. + * This class is used for image formats that are restricted to one image per file. + * Examples: PNG and BMP image formats. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +class SingleImageStore extends WorldFileStore implements GridCoverageResource { + /** + * The singleton resource in this aggregate. Fetched when first needed. + */ + private volatile WorldFileResource delegate; + + /** + * 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. + * @throws DataStoreException if an error occurred while opening the stream. + * @throws IOException if an error occurred while creating the image reader instance. + */ + SingleImageStore(final FormatFinder format) throws DataStoreException, IOException { + super(format, false); + } + + /** + * Returns {@code true} for meaning that the singleton component will be used only for internal purposes. + */ + @Override + final boolean isComponentHidden() { + return true; + } + + /** + * Returns the singleton resource in this aggregate. The delegate is used for all + * {@link GridCoverageResource} operations but <strong>not</strong> for the following operations: + * + * <ul> + * <li>{@link #getIdentifier()} because we want the filename without ":1" suffix (the image index).</li> + * <li>{@link #getMetadata()} because it is richer than {@link WorldFileResource#getMetadata()}.</li> + * </ul> + */ + final WorldFileResource delegate() throws DataStoreException { + WorldFileResource r = delegate; + if (r == null) { + delegate = r = ((Components) components()).get(MAIN_IMAGE); + } + return r; + } + + /** + * Returns the valid extent of grid coordinates together with the conversion from those grid coordinates + * to real world coordinates. The CRS and "pixels to CRS" conversion may be unknown if the {@code *.prj} + * and/or world auxiliary file has not been found. + */ + @Override + public final GridGeometry getGridGeometry() throws DataStoreException { + return delegate().getGridGeometry(); + } + + /** + * Returns the envelope of the grid geometry if known. + * The envelope is absent if the grid geometry does not provide this information. + */ + @Override + public final Optional<Envelope> getEnvelope() throws DataStoreException { + return delegate().getEnvelope(); + } + + /** + * Returns the preferred resolutions (in units of CRS axes) for read operations in this data store. + */ + @Override + public final List<double[]> getResolutions() throws DataStoreException { + return delegate().getResolutions(); + } + + /** + * Returns the ranges of sample values in each band. Those sample dimensions describe colors + * because the World File format does not provide more information. + */ + @Override + public final List<SampleDimension> getSampleDimensions() throws DataStoreException { + return delegate().getSampleDimensions(); + } + + /** + * Requests a subset of the coverage. + */ + @Override + public final GridCoverageResource subset(Query query) throws UnsupportedQueryException, DataStoreException { + return delegate().subset(query); + } + + /** + * Loads a subset of the image wrapped by this resource. + * + * @param domain desired grid extent and resolution, or {@code null} for reading the whole domain. + * @param range 0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all. + * @return the grid coverage for the specified domain and range. + */ + @Override + public final GridCoverage read(GridGeometry domain, int... range) throws DataStoreException { + return delegate().read(domain, range); + } + + /** + * Returns an indication about when the "physical" loading of raster data will happen. + */ + @Override + public final RasterLoadingStrategy getLoadingStrategy() throws DataStoreException { + return delegate().getLoadingStrategy(); + } + + /** + * Sets the preferred strategy about when to do the "physical" loading of raster data. + * Implementations are free to ignore this parameter or to replace the given strategy + * by the closest alternative that this resource can support. + * + * @param strategy the desired strategy for loading raster data. + * @return {@code true} if the given strategy has been accepted, or {@code false} + * if this implementation replaced the given strategy by an alternative. + */ + @Override + public final boolean setLoadingStrategy(RasterLoadingStrategy strategy) throws DataStoreException { + return delegate().setLoadingStrategy(strategy); + } + + /** + * The writable variant of {@link MultiImageStore}. + */ + static final class Writable extends SingleImageStore implements WritableGridCoverageResource { + /** + * 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. + * @throws DataStoreException if an error occurred while opening the stream. + * @throws IOException if an error occurred while creating the image reader instance. + */ + Writable(final FormatFinder format) throws DataStoreException, IOException { + super(format); + } + + /** + * Writes a new coverage in the data store for this resource. If a coverage already exists for this resource, + * then it will be overwritten only if the {@code TRUNCATE} or {@code UPDATE} option is specified. + * + * @param coverage new data to write in the data store for this resource. + * @param options configuration of the write operation. + */ + @Override + public void write(GridCoverage coverage, Option... options) throws DataStoreException { + ((WritableResource) delegate()).write(coverage, options); + } + } +} diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java index 05da7f95f6..8c2f9e53ec 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java @@ -117,7 +117,7 @@ class WorldFileResource extends AbstractGridCoverageResource implements StoreRes WorldFileResource(final WorldFileStore store, final StoreListeners parent, final int imageIndex, final GridGeometry gridGeometry) { - super(parent, false); + super(parent, store.isComponentHidden()); this.store = store; this.imageIndex = imageIndex; this.gridGeometry = gridGeometry; @@ -208,7 +208,8 @@ class WorldFileResource extends AbstractGridCoverageResource implements StoreRes } /** - * Returns the ranges of sample values. + * Returns the ranges of sample values in each band. Those sample dimensions describe colors + * because the World File format does not provide more information. */ @Override @SuppressWarnings("ReturnOfCollectionOrArrayField") diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java index 4ff7398186..0d48e9e8ef 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java @@ -20,7 +20,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.logging.Level; import java.io.IOException; import java.io.EOFException; @@ -32,8 +31,6 @@ import java.nio.file.NoSuchFileException; import java.nio.file.StandardOpenOption; import javax.imageio.ImageIO; import javax.imageio.ImageReader; -import javax.imageio.ImageWriter; -import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import org.opengis.metadata.Metadata; import org.opengis.metadata.maintenance.ScopeCode; @@ -53,7 +50,6 @@ import org.apache.sis.storage.ReadOnlyStorageException; import org.apache.sis.storage.UnsupportedStorageException; import org.apache.sis.internal.storage.Resources; import org.apache.sis.internal.storage.PRJDataStore; -import org.apache.sis.internal.storage.io.IOUtilities; import org.apache.sis.internal.referencing.j2d.AffineTransform2D; import org.apache.sis.internal.storage.MetadataBuilder; import org.apache.sis.internal.util.ListOfUnknownSize; @@ -109,13 +105,16 @@ import org.apache.sis.setup.OptionKey; * Because some image formats can store an arbitrary amount 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) * @version 1.2 * @since 1.2 * @module */ -class WorldFileStore extends PRJDataStore implements Aggregate { +class WorldFileStore extends PRJDataStore { /** * Image I/O format names (ignoring case) for which we have an entry in the {@code SpatialMetadata} database. */ @@ -217,84 +216,45 @@ class WorldFileStore extends PRJDataStore implements Aggregate { * * @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>). - * @param readOnly whether to fail if the channel can not be opened at least in read mode. * @throws DataStoreException if an error occurred while opening the stream. * @throws IOException if an error occurred while creating the image reader instance. */ - WorldFileStore(final WorldFileStoreProvider provider, final StorageConnector connector, final boolean readOnly) + public WorldFileStore(final WorldFileStoreProvider provider, final StorageConnector connector) throws DataStoreException, IOException { - super(provider, connector); - identifiers = new HashMap<>(); + 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 for RFE #4093999 in Sun's bug database, for allowing us to invoke + * {@link FormatFinder#cleanup()} when invoked from the public constructor. + * @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.useWarningEventsOnly(); - final Object storage = connector.getStorage(); - if (storage instanceof ImageReader) { - reader = (ImageReader) storage; - suffix = IOUtilities.extension(reader.getInput()); + identifiers = new HashMap<>(); + suffix = format.suffix; + 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(); - return; - } - if (storage instanceof ImageWriter) { - suffix = IOUtilities.extension(((ImageWriter) storage).getOutput()); - return; - } - suffix = IOUtilities.extension(storage); - if (!(readOnly || fileExists(connector))) { + if (readOnly) { + format.close(); + } /* - * If the store is opened in read-write mode, create the image reader only - * if the file exists and is non-empty. Otherwise we let `reader` to null - * and the caller will create an image writer instead. + * 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. */ - return; } - /* - * Search for a reader that claim to be able to read the storage input. - * First we try readers associated to the file suffix. If no reader is - * found, we try all other readers. - */ - final Map<ImageReaderSpi,Boolean> deferred = new LinkedHashMap<>(); - if (suffix != null) { - reader = FormatFilter.SUFFIX.createReader(suffix, connector, deferred); - } - if (reader == null) { - reader = FormatFilter.SUFFIX.createReader(null, connector, deferred); -fallback: if (reader == null) { - /* - * If no reader has been found, maybe `StorageConnector` has not been able to create - * an `ImageInputStream`. It may happen if the storage object is of unknown type. - * Check if it is the case, then try all providers that we couldn't try because of that. - */ - ImageInputStream stream = null; - for (final Map.Entry<ImageReaderSpi,Boolean> entry : deferred.entrySet()) { - if (entry.getValue()) { - if (stream == null) { - if (!readOnly) { - // ImageOutputStream is both read and write. - stream = ImageIO.createImageOutputStream(storage); - } - if (stream == null) { - stream = ImageIO.createImageInputStream(storage); - if (stream == null) break; - } - } - final ImageReaderSpi p = entry.getKey(); - if (p.canDecodeInput(stream)) { - connector.closeAllExcept(storage); - reader = p.createReaderInstance(); - reader.setInput(stream); - break fallback; - } - } - } - throw new UnsupportedStorageException(super.getLocale(), WorldFileStoreProvider.NAME, - storage, connector.getOption(OptionKey.OPEN_OPTIONS)); - } - } - configureReader(); - /* - * 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. - */ } /** @@ -567,7 +527,6 @@ loop: for (int convention=0;; convention++) { * * @return list of images in this store. */ - @Override @SuppressWarnings("ReturnOfCollectionOrArrayField") public final synchronized Collection<? extends GridCoverageResource> components() throws DataStoreException { if (components == null) try { @@ -762,6 +721,15 @@ loop: for (int convention=0;; convention++) { 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. diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java index 5306122bb2..e9c3959cd8 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java @@ -18,10 +18,7 @@ package org.apache.sis.internal.storage.image; import java.util.Set; import java.util.HashSet; -import java.io.DataOutput; import java.io.IOException; -import javax.imageio.ImageReader; -import javax.imageio.ImageWriter; import javax.imageio.spi.ImageReaderSpi; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.StorageConnector; @@ -30,7 +27,9 @@ import org.apache.sis.internal.storage.StoreMetadata; import org.apache.sis.internal.storage.PRJDataStore; import org.apache.sis.internal.storage.io.IOUtilities; import org.apache.sis.storage.GridCoverageResource; +import org.apache.sis.storage.Aggregate; import org.apache.sis.storage.ProbeResult; +import org.apache.sis.util.ArraysExt; /** @@ -51,10 +50,42 @@ public final class WorldFileStoreProvider extends PRJDataStore.Provider { */ static final String NAME = "World file"; + /** + * Name of image formats that are considered to allow only one image. + * There is no public Image I/O API giving this information, so we have to use a hard-coded list. + * All formats not in this list are assumed to allow more than one image. + * + * <h4>Case of JPEG</h4> + * The JPEG image reader implementation in standard JDK seems to count a number of images that can be anything. + * However documentation on the web often describes the JPEG format as a container for a single image. + * It is not clear if we should include JPEG in this list or not. + */ + private static final String[] SINGLE_IMAGE_FORMATS = {"PNG", "BMP", "WBMP", "JPEG"}; + + /** + * Whether the provider is allowed to create {@link GridCoverageResource} instances + * instead of {@link Aggregate} instances. + */ + private final boolean allowSingleton; + /** * Creates a new provider. */ public WorldFileStoreProvider() { + allowSingleton = true; + } + + /** + * Creates a new provider with the given configuration. + * If {@code allowSingleton} is {@code false}, then this provider will unconditionally create + * {@link WorldFileStore} instances that implement the {@link Aggregate} interface, regardless + * if the image format allows many pictures or not. + * + * @param allowSingleton whether the provider is allowed to create {@code GridCoverageResource} instances + * instead of {@code Aggregate} instances. + */ + public WorldFileStoreProvider(final boolean allowSingleton) { + this.allowSingleton = allowSingleton; } /** @@ -78,25 +109,29 @@ public final class WorldFileStoreProvider extends PRJDataStore.Provider { */ @Override public WorldFileStore open(final StorageConnector connector) throws DataStoreException { - final Object storage = connector.getStorage(); - boolean isWritable = (storage instanceof ImageWriter); - if (!isWritable) { - if (storage instanceof ImageReader) { - Object input = ((ImageReader) storage).getInput(); - isWritable = (input instanceof DataOutput); // Parent of ImageOutputStream. - } else { - isWritable = isWritable(connector); + final WorldFileStore store; + try (FormatFinder format = new FormatFinder(this, connector)) { + boolean isSingleton = false; + if (allowSingleton) { + final String[] names = format.getFormatName(); + if (names != null) { + for (final String name : names) { + isSingleton = ArraysExt.containsIgnoreCase(SINGLE_IMAGE_FORMATS, name); + if (isSingleton) break; + } + } } - } - try { - if (isWritable) { - return new WritableStore(this, connector); + if (format.isWritable) { + store = isSingleton ? new SingleImageStore.Writable(format) + : new MultiImageStore.Writable(format); } else { - return new WorldFileStore(this, connector, true); + store = isSingleton ? new SingleImageStore(format) + : new MultiImageStore(format); } } catch (IOException e) { throw new DataStoreException(e); } + return store; } /** diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java index bd7fe0078c..3d74d0b54e 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java @@ -16,10 +16,7 @@ */ package org.apache.sis.internal.storage.image; -import java.util.Map; -import java.util.LinkedHashMap; import java.util.function.BiConsumer; -import java.io.File; import java.io.IOException; import java.io.BufferedWriter; import java.nio.file.StandardOpenOption; @@ -33,7 +30,6 @@ import javax.imageio.spi.ImageWriterSpi; import javax.imageio.spi.ImageReaderWriterSpi; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; -import javax.imageio.stream.FileImageOutputStream; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridExtent; @@ -69,12 +65,20 @@ import org.apache.sis.setup.OptionKey; * The image writer will be {@linkplain ImageWriter#dispose() disposed} * and its output 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 amount 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 WritableAggregate} 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) * @version 1.2 * @since 1.2 * @module */ -final class WritableStore extends WorldFileStore implements WritableAggregate { +class WritableStore extends WorldFileStore { /** * Position of the input/output stream beginning. This is usually 0. */ @@ -101,68 +105,28 @@ final class WritableStore extends WorldFileStore implements WritableAggregate { /** * 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>). + * @param format information about the storage (URL, stream, <i>etc</i>) and the reader/writer to use. * @throws DataStoreException if an error occurred while opening the stream. * @throws IOException if an error occurred while creating the image reader instance. */ - WritableStore(final WorldFileStoreProvider provider, final StorageConnector connector) - throws DataStoreException, IOException - { - super(provider, connector, false); - final Object storage = connector.getStorage(); - final ImageReader reader = getCurrentReader(); - final Object inout; - if (reader != null) { - inout = reader.getInput(); - numImages = -1; - } else if (storage instanceof ImageWriter) { - writer = (ImageWriter) storage; - inout = writer.getOutput(); - configureWriter(); + WritableStore(final FormatFinder format) throws DataStoreException, IOException { + super(format, false); + if (getCurrentReader() != null) { numImages = -1; } else { - /* - * If it was possible to initialize an image reader, wait to see if an image writer is needed. - * Otherwise (i.e. if the destination file does not exist), create the image writer immediately. - * The code below is a copy of the code in parent class constructor (for creating `ImageReader`), - * but adapted to the case of creating an `ImageWriter`. - */ - final Map<ImageWriterSpi,Boolean> deferred = new LinkedHashMap<>(); - if (suffix != null) { - writer = FormatFilter.SUFFIX.createWriter(suffix, connector, null, deferred); - } + writer = format.getOrCreateWriter(); if (writer == null) { - writer = FormatFilter.SUFFIX.createWriter(null, connector, null, deferred); -fallback: if (writer == null) { - ImageOutputStream stream = null; - for (final Map.Entry<ImageWriterSpi,Boolean> entry : deferred.entrySet()) { - if (entry.getValue()) { - if (stream == null) { - final File file = connector.getStorageAs(File.class); - if (file != null) { - stream = new FileImageOutputStream(file); - } else { - stream = ImageIO.createImageOutputStream(storage); - if (stream == null) break; - } - } - final ImageWriterSpi p = entry.getKey(); - connector.closeAllExcept(storage); - writer = p.createWriterInstance(); - writer.setOutput(stream); - break fallback; - } - } - throw new UnsupportedStorageException(super.getLocale(), WorldFileStoreProvider.NAME, - storage, connector.getOption(OptionKey.OPEN_OPTIONS)); - } + throw new UnsupportedStorageException(super.getLocale(), WorldFileStoreProvider.NAME, + format.storage, format.connector.getOption(OptionKey.OPEN_OPTIONS)); } configureWriter(); - inout = writer.getOutput(); - // Leave `numImages` to 0 because we know that the stream is empty. + if (!format.fileIsEmpty) { + numImages = -1; + } else { + // Leave `numImages` to 0. + } } - streamBeginning = (inout instanceof ImageInputStream) ? ((ImageInputStream) inout).getStreamPosition() : 0; + streamBeginning = (format.storage instanceof ImageInputStream) ? ((ImageInputStream) format.storage).getStreamPosition() : 0; } /** @@ -303,7 +267,6 @@ writeCoeffs: for (int i=0;; i++) { * @return the effectively added resource. * @throws DataStoreException if the given resource can not be stored in this {@code Aggregate}. */ - @Override public synchronized Resource add(final Resource resource) throws DataStoreException { Exception cause = null; if (resource instanceof GridCoverageResource) try { diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java index 62778d2c60..5da4c06fd3 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java @@ -55,7 +55,7 @@ public final strictfp class SelfConsistencyTest extends CoverageReadConsistency public static void openFile() throws IOException, DataStoreException { final URL url = WorldFileStoreTest.class.getResource("gradient.png"); assertNotNull("Test file not found.", url); - store = new WorldFileStore(null, new StorageConnector(url), true); + store = new WorldFileStore(null, new StorageConnector(url)); } /** diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java index ec967d2d67..9c174d5241 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java @@ -76,7 +76,15 @@ public final strictfp class WorldFileStoreTest extends TestCase { public void testMetadata() throws DataStoreException { final WorldFileStoreProvider provider = new WorldFileStoreProvider(); try (WorldFileStore store = provider.open(testData())) { + /* + * Opportunistic check of store type. Should be read-only, + * and should have been simplified to the "single image" case. + */ assertFalse(store instanceof WritableStore); + assertTrue(store instanceof SingleImageStore); + /* + * Verify metadata content. + */ assertEquals("gradient", store.getIdentifier().get().toString()); final Metadata metadata = store.getMetadata(); final Identification id = getSingleton(metadata.getIdentificationInfo()); @@ -98,6 +106,8 @@ public final strictfp class WorldFileStoreTest extends TestCase { /** * Tests reading the coverage and writing it in a new file. + * This test unconditionally open the data store as an aggregate, + * i.e. it bypasses the simplification of PNG files as {@link SingleImageStore} view. * * @throws DataStoreException if an error occurred during Image I/O or data store operations. * @throws IOException if an error occurred when creating, reading or deleting temporary files. @@ -106,7 +116,7 @@ public final strictfp class WorldFileStoreTest extends TestCase { public void testReadWrite() throws DataStoreException, IOException { final Path directory = Files.createTempDirectory("SIS-"); try { - final WorldFileStoreProvider provider = new WorldFileStoreProvider(); + final WorldFileStoreProvider provider = new WorldFileStoreProvider(false); try (WorldFileStore source = provider.open(testData())) { assertFalse(source instanceof WritableStore); final GridCoverageResource resource = getSingleton(source.components());